diff --git a/apple/oskiewar/Sources/MetalSceneView.swift b/apple/oskiewar/Sources/MetalSceneView.swift index 8fcdf3a171..d8a33896ef 100644 --- a/apple/oskiewar/Sources/MetalSceneView.swift +++ b/apple/oskiewar/Sources/MetalSceneView.swift @@ -88,16 +88,21 @@ final class MetalSceneView: MTKView, MTKViewDelegate { #include using namespace metal; struct Vertex { float4 position; float4 color; }; - struct Raster { float4 position [[position]]; float3 color; }; + struct Raster { float4 position [[position]]; float4 color; }; vertex Raster scene_vertex(const device Vertex *vertices [[buffer(0)]], uint id [[vertex_id]]) { Raster out; out.position = vertices[id].position; - out.color = vertices[id].color.rgb; + out.color = vertices[id].color; return out; } fragment float4 scene_fragment(Raster in [[stage_in]]) { - return float4(in.color, 1.0); + if(in.color.a<0.999){ + uint2 pixel=uint2(in.position.xy)&3; + const uint pattern[16]={0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5}; + if(in.color.a<(pattern[pixel.y*4+pixel.x]+0.5)/16.0)discard_fragment(); + } + return float4(in.color.rgb, 1.0); } struct DecalVertex { float4 position; float4 uvq; }; struct DecalRaster { float4 position [[position]]; float3 uvq; }; @@ -200,7 +205,7 @@ final class MetalSceneView: MTKView, MTKViewDelegate { defer { sceneLock.unlock() } let ink = SIMD4(Float(clamped255(values[9])), Float(clamped255(values[10])), - Float(clamped255(values[11])), 1) + Float(clamped255(values[11])), values.count>12 ? Float(max(0,min(1,values[12]))) : 1) for vertex in 0..<3 { let at = vertex * 3 let x = Float(values[at] / Double(logicalSize.width / 2) - 1) diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index ac99d34b3f..2e16026ef3 100644 --- a/xbox/live/oskiewar.js +++ b/xbox/live/oskiewar.js @@ -102,7 +102,7 @@ if (hostAnalytics) const hostAc = typeof ac === "function" ? ac : null; // Monotonic count of committed revisions to this piece (next revision included). -const buildVersion = 202; +const buildVersion = 203; const floorY = 1800; // Oskiewar now opens as a versus game. An ordinary web visit hosts a room — // the URL becomes the invitation — and until a friend opens it, all you can @@ -2861,6 +2861,9 @@ function projectedTriangle(a, b, c, color) { // Positional, not spread: see the note on emitTriangle above. Every face a // frame submits comes through here, and `...color` built one throwaway // iterator apiece for ~2100 of them. + if(color[3]!==undefined&&typeof triangleAlpha==='function'){ + triangleAlpha(a.x,a.y,a.z,b.x,b.y,b.z,c.x,c.y,c.z,color[0],color[1],color[2],color[3]);return; + } if (worldDepthMode === 0) { emitTriangle(a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, color[0], color[1], color[2]); @@ -12621,6 +12624,23 @@ function updatePoolPlayer(p, pad, dt, now) { const axis=value=>Math.abs(value)>.18?Math.sign(value)*(Math.abs(value)-.18)/.82:0; const turn=Number(held.includes('ArrowRight'))-Number(held.includes('ArrowLeft')) || axis(pad.leftX||0); const throttle=Number(held.includes('ArrowUp'))-Number(held.includes('ArrowDown')) || axis(pad.leftY||0); + // Drawing has Cartesian controls, independent of the rider's heading. + // B + directions never enters the steering/double-tap dash recognizer. + if(p.chalkColor&&held.includes('B')&&p.grounded&&!p.swimming){ + if(p.skateboard)dismountSkateboard(p,now); + const before={x:p.x,z:p.z||0},length=Math.max(1,Math.hypot(turn,throttle)); + p.vx=-turn/length*360;p.vz=throttle/length*360;p.vy=0; + p.x+=p.vx*dt;p.z=(p.z||0)+p.vz*dt; + boundParkBody(p,before);p.y=poolFloorAt(p.x,p.z); + p.poolTap=p.poolAnalogTap=null;p.poolAnalogKey='';p.poolLastSteer=0;p.poolRunTime=0; + p.poolPipeLocked=false;p.poolVert=null;p.dashUntil=0;p.attackKind='';p.blocking=false; + p.inputX=turn;p.inputZ=throttle;p.chalkDrawing=true; + if(held.includes('RightShoulder')&&!p.spin)p.spin={angle:0,rate:6,direction:1,startedAt:now}; + if(held.includes('RightShoulder')&&p.spin)p.spin.rate=6; + updateSpin(p,dt);updateChalk(p,held,now); + p.poolStridePhase=(p.poolStridePhase||0)+Math.hypot(p.vx,p.vz)*dt/240; + p.stance=Math.hypot(p.vx,p.vz)>1?'WALK':'NEUTRAL';p.previous=held.slice();return; + } const tapKey=['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].find(pressed); let directionDash=false; if(tapKey){directionDash=p.poolTap?.key===tapKey&&now-p.poolTap.at<280000;p.poolTap=directionDash?null:{key:tapKey,at:now};} @@ -20538,9 +20558,9 @@ function drawParkBuilding(){ drawParkScene(parkBuildingMesh); // The overhead view cuts away roof, beams and hanging lights, while their // physical breakable surface remains in the simulation. - if(cameraDoll.position.y>parkDeckY-parkBuildingHeight+100){ + if(globalThis.meshTransparency===true||typeof triangleAlpha==='function'||cameraDoll.position.y>parkDeckY-parkBuildingHeight+100){ if(!parkRoofMesh)parkRoofMesh=captureQuadMesh(drawParkRoofGeometry); - drawParkScene(parkRoofMesh); + drawParkScene(parkRoofMesh,.18); } drawParkWindows();drawParkWindowShards(); } @@ -23713,7 +23733,7 @@ function drawSeatPlayerHud(ink){ for(const p of activePlayers()){ if(p.dummy)continue; const i=p.pad,{idle,measure,metrics}=seatHudReadout(p,now); - if(poolOnly()&&i===0){const drop=parkDropName(p);if(drop)seatHudText('L3 drop '+drop,safe.left,safe.bottom-42,42,[255,186,126]);const action=p.chalkColor?'B draw':p.axeHeld?'B swing':'B punch',fire=p.gunAmmo>0?'Y shoot':'Y kick';for(const [j,text] of [action+' / '+fire,(p.skateboard?'A dismount':'A jump')+' / RB block'].entries())seatHudText(text,safe.right-handleWidth(text,42),safe.bottom-92+j*50,42,[200,215,232]);} + if(poolOnly()&&i===0){const drop=parkDropName(p);if(drop)seatHudText('L3 drop '+drop,safe.left,safe.bottom-42,42,[255,186,126]);const action=p.chalkColor?'B draw':p.axeHeld?'B swing':'B punch',fire=p.gunAmmo>0?'Y shoot':'Y kick';for(const [j,text] of [action+' / '+fire,p.chalkDrawing?'RB spin':(p.skateboard?'A dismount':'A jump')+' / RB block'].entries())seatHudText(text,safe.right-handleWidth(text,42),safe.bottom-92+j*50,42,[200,215,232]);} const accent=p.wheelTurbo?[209,129,255]:i?[174,161,255]:[255,144,188]; const size=42; const heartRadius=size*.23,heartSpace=heartRadius*3+18; @@ -23859,11 +23879,11 @@ function buildParkScene(mesh){ }; return build(entries); } -function drawParkScene(mesh){ +function drawParkScene(mesh,opacity=1){ if(!mesh)return; mesh.sceneRoot ||= buildParkScene(mesh); const visit=node=>{if(!sceneBoundsVisible(node.bounds))return; - if(node.children)for(const child of node.children)visit(child);else drawQuadMesh(node.mesh);}; + if(node.children)for(const child of node.children)visit(child);else drawQuadMesh(node.mesh,opacity);}; visit(mesh.sceneRoot); // Coping rails are few and clip independently; their widths remain physical. for(const a of mesh.capsules||[])worldCapsule(a[0],a[1],a[2],a[3],a[4],a[5],a[6]*cameraScale(),a[7],a[8]); @@ -23892,13 +23912,13 @@ function releaseQuadMesh(mesh){ if(mesh&&mesh.nativeHandle>=0&&nativeMeshFree)nativeMeshFree(mesh.nativeHandle); if(mesh)mesh.nativeHandle=-1; } -function drawQuadMesh(mesh){ +function drawQuadMesh(mesh,opacity=1){ if(!mesh||(!nativeRetainedMeshes&&!sceneBoundsVisible(mesh.bounds)))return; if(nativeRetainedMeshes){ const handle=meshHandle(mesh); - if(handle>=0){nativeMeshDraw(handle,mainNativeCamera(),clipView?0:cameraScale());return;} + if(handle>=0){nativeMeshDraw(handle,mainNativeCamera(),clipView?0:cameraScale(),1,1,1,opacity);return;} } - if(nativeSceneMesh){ + if(nativeSceneMesh&&opacity===1){ if(!clipView){const scale=cameraScale();for(const a of mesh.capsules||[])worldCapsule(a[0],a[1],a[2],a[3],a[4],a[5],a[6]*scale,a[7],a[8]);} const buffers=nativeMeshBuffers(mesh);nativeSceneMesh(buffers.vertices,buffers.faces,mainNativeCamera());return; } @@ -23907,7 +23927,7 @@ function drawQuadMesh(mesh){ } const meshScale=cameraScale(); if(!clipView)for(const args of mesh.capsules||[])worldCapsule(args[0],args[1],args[2],args[3],args[4],args[5],args[6]*meshScale,args[7],args[8]); - if(programBuffered){ + if(programBuffered&&opacity===1){ // Sent once, drawn by handle: the interpreter keeps the mesh, lights each // quad and takes it to the camera. The bounds cull above already dropped // a mesh that is wholly off screen. @@ -23926,7 +23946,7 @@ function drawQuadMesh(mesh){ const [ia,ib,ic,id]=f.ids,a=mesh.projected[ia],b=mesh.projected[ib],c=mesh.projected[ic],d=mesh.projected[id]; if(a.near&&b.near&&c.near&&d.near&&((a.x<0&&b.x<0&&c.x<0&&d.x<0)||(a.x>stageRight&&b.x>stageRight&&c.x>stageRight&&d.x>stageRight)||(a.y<0&&b.y<0&&c.y<0&&d.y<0)||(a.y>viewHeight&&b.y>viewHeight&&c.y>viewHeight&&d.y>viewHeight)))continue; const n=f.normal,k=.72+Math.max(0,-n[0]*globalLight.x-n[1]*globalLight.y-n[2]*globalLight.z)*.28; - const color=f.lit;for(let i=0;i<3;i++)color[i]=Math.round(f.color[i]*k); + const color=f.lit;color[3]=opacity;for(let i=0;i<3;i++)color[i]=Math.round(f.color[i]*k); if(a.near&&b.near&&c.near&&d.near&&bandContains(a)&&bandContains(b)&&bandContains(c)&&bandContains(d)){ projectedTriangle(a,b,c,color);projectedTriangle(a,c,d,color); }else{worldTriangle(mesh.vertices[ia],mesh.vertices[ib],mesh.vertices[ic],color);worldTriangle(mesh.vertices[ia],mesh.vertices[ic],mesh.vertices[id],color);} diff --git a/xbox/live/tests/pool-playground.test.mjs b/xbox/live/tests/pool-playground.test.mjs index f452ae6937..8c027a2760 100644 --- a/xbox/live/tests/pool-playground.test.mjs +++ b/xbox/live/tests/pool-playground.test.mjs @@ -647,3 +647,12 @@ test('two held items occupy different arms and block another pickup until droppe assert.equal(a.freeItemArm(p,'chalk'),'');assert.equal(a.freeItemArm(p,'gun'),''); a.dropParkItem(p,a.now());assert.ok(a.freeItemArm(p,'chalk'));assert.equal(p.gunAmmo,5); }); + +test('drawing directions translate directly regardless of facing and never double-tap dash',()=>{ + const a=playground(),p=a.players[0];p.chalkColor=a.chalkColors[0];p.poolYaw=1.7; + const x=p.x,z=p.z,yaw=p.poolYaw; + a.step(['B','ArrowRight']);assert.ok(p.x Void = + { [weak self] a,b,c,d,e,f,g,h,i,r,green,blue,alpha in + guard let self = self else { return } + _ = self.renderer.scene.triangle([a,b,c,d,e,f,g,h,i,r,green,blue,alpha], logicalSize: CGSize(width: 1920, height: 1080)) + } let text: (String) -> @convention(block) (String, Double, Double, Double, Double, Double, Double) -> Void = { family in { [weak self] value, x, y, size, r, g, b in self?.renderer.write(value, family: family, x: x, y: y, size: size, r, g, b) @@ -991,6 +996,7 @@ private final class NativeGameHost { javascript.setObject(box, forKeyedSubscript: "box" as NSString) javascript.setObject(line, forKeyedSubscript: "line" as NSString) javascript.setObject(triangle, forKeyedSubscript: "triangle" as NSString) + javascript.setObject(triangleAlpha, forKeyedSubscript: "triangleAlpha" as NSString) javascript.setObject(triangle3d, forKeyedSubscript: "triangle3d" as NSString) javascript.setObject(text("Comic Relief"), forKeyedSubscript: "comicWrite" as NSString) javascript.setObject(text("Comic Relief"), forKeyedSubscript: "ywftWrite" as NSString) diff --git a/xbox/native-bios/App.cpp b/xbox/native-bios/App.cpp index a66885841f..15bff9b21d 100644 --- a/xbox/native-bios/App.cpp +++ b/xbox/native-bios/App.cpp @@ -890,7 +890,7 @@ private: x / 960.0f - 1.0f, 1.0f - y / 540.0f, (std::max)(0.0f, (std::min)(1.0f, (z + 1.5f) / 3.0f)), - color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, 1.0f, + color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f, }; }; for (const auto& triangle : m_frameTriangles) { diff --git a/xbox/native-bios/Package.appxmanifest b/xbox/native-bios/Package.appxmanifest index 4f525b910b..9c1ea1b5f0 100644 --- a/xbox/native-bios/Package.appxmanifest +++ b/xbox/native-bios/Package.appxmanifest @@ -6,7 +6,7 @@ IgnorableNamespaces="uap mp rescap"> + Version="1.0.0.60" /> diff --git a/xbox/native-bios/QuickJsEngine.cpp b/xbox/native-bios/QuickJsEngine.cpp index 104302516f..0bcfeb86a1 100644 --- a/xbox/native-bios/QuickJsEngine.cpp +++ b/xbox/native-bios/QuickJsEngine.cpp @@ -1014,8 +1014,9 @@ class QuickJsPiece final : public JsPiece { JS_SetPropertyStr(context_, global, "capsule3d", JS_NewCFunction(context_, SceneCapsule, "capsule3d", 9)); JS_SetPropertyStr(context_, global, "sceneMesh", JS_NewCFunction(context_, SceneMesh, "sceneMesh", 3)); JS_SetPropertyStr(context_, global, "meshUpload", JS_NewCFunction(context_, MeshUpload, "meshUpload", 3)); - JS_SetPropertyStr(context_, global, "meshDraw", JS_NewCFunction(context_, MeshDraw, "meshDraw", 6)); + JS_SetPropertyStr(context_, global, "meshDraw", JS_NewCFunction(context_, MeshDraw, "meshDraw", 7)); JS_SetPropertyStr(context_, global, "meshFree", JS_NewCFunction(context_, MeshFree, "meshFree", 1)); + JS_SetPropertyStr(context_, global, "meshTransparency", JS_NewBool(context_, true)); JS_SetPropertyStr(context_, global, "triangle3d", JS_NewCFunction(context_, Triangle3d, "triangle3d", 12)); JS_SetPropertyStr(context_, global, "triangles3d", JS_NewCFunction(context_, Triangles3d, "triangles3d", 2)); JS_SetPropertyStr(context_, global, "sprites3d", JS_NewCFunction(context_, Sprites3d, "sprites3d", 2)); diff --git a/xbox/native-bios/ScenePrimitives.inc b/xbox/native-bios/ScenePrimitives.inc index b6a79a1a69..3a31a79a4c 100644 --- a/xbox/native-bios/ScenePrimitives.inc +++ b/xbox/native-bios/ScenePrimitives.inc @@ -186,8 +186,8 @@ JSValue MeshDraw(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ auto& store=scope->scene.meshes; if(handle<0||size_t(handle)>=store.size()||!store[handle])return JS_NewInt32(ctx,0); if(!camera.read(args[1])||camera.count!=27||!SceneCameraValid(camera.data))return JS_ThrowRangeError(ctx,"meshDraw needs a 27-float camera"); - double extra[4]{1,1,1,1}; - for(int i=0;i<4&&argc>2+i;i++){if(JS_ToFloat64(ctx,&extra[i],args[2+i])||!std::isfinite(extra[i]))return JS_ThrowRangeError(ctx,"meshDraw scale/tint");} + double extra[5]{1,1,1,1,1}; + for(int i=0;i<5&&argc>2+i;i++){if(JS_ToFloat64(ctx,&extra[i],args[2+i])||!std::isfinite(extra[i]))return JS_ThrowRangeError(ctx,"meshDraw scale/tint");} const double capsuleScale=std::clamp(extra[0],0.0,1024.0);const double tint[3]{std::clamp(extra[1],0.0,4.0),std::clamp(extra[2],0.0,4.0),std::clamp(extra[3],0.0,4.0)}; const bool tinted=tint[0]!=1||tint[1]!=1||tint[2]!=1; RetainedMesh& mesh=*store[handle];const float* m=camera.data;const double near=m[17]; @@ -212,7 +212,8 @@ JSValue MeshDraw(JSContext* ctx,JSValueConst,int argc,JSValueConst* args){ int emitted=0;ScenePoint a[9],b[9]; for(size_t i=0;im[20]+guard&&to.x>m[20]+guard)||(from.ym[21]+guard&&to.y>m[21]+guard))continue; Color color=SceneColor(c[7],c[8],c[9]); - if(tinted)color=SceneColor(color.r*tint[0],color.g*tint[1],color.b*tint[2]); + if(tinted){const auto alpha=color.a;color=SceneColor(color.r*tint[0],color.g*tint[1],color.b*tint[2]);color.a=alpha;} const double width=std::max(1.0,double(c[6])*capsuleScale),depth=std::clamp(std::min(from.z,to.z)+double(c[10]),-1.499,1.4); const double radius=width*.5;const int fan=radius<6?3:radius<13?4:radius<26?6:radius<52?8:12; SceneCapsuleFan(g,from.x,from.y,to.x,to.y,depth,width,color);emitted+=2+2*fan; diff --git a/xbox/native-bios/TrianglePixelShader.hlsl b/xbox/native-bios/TrianglePixelShader.hlsl index 0d6b772e11..41edea23e0 100644 --- a/xbox/native-bios/TrianglePixelShader.hlsl +++ b/xbox/native-bios/TrianglePixelShader.hlsl @@ -4,5 +4,12 @@ struct PixelInput { }; float4 main(PixelInput input) : SV_TARGET { - return input.color; + // Screen-door transparency keeps depth correct without sorting or an + // additional full-screen pass. FXAA softens the fine coverage pattern. + if(input.color.a<0.999){ + const uint2 pixel=uint2(input.position.xy)&3; + const uint pattern[16]={0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5}; + clip(input.color.a-(pattern[pixel.y*4+pixel.x]+0.5)/16.0); + } + return float4(input.color.rgb,1.0); }