ฉันจะอ่านระหว่างค่าที่วนซ้ำ (เช่นสีหรือการหมุน) ได้อย่างไร


25

ตัวอย่างภาพเคลื่อนไหวร่วม

ดูการสาธิต

ฉันพยายามที่จะทำให้รอยต่อหมุนได้อย่างราบรื่นรอบจุดศูนย์กลางของผืนผ้าใบไปยังมุมของตัวชี้เมาส์ สิ่งที่ฉันได้ผล แต่ฉันต้องการให้มันเคลื่อนไหวได้ในระยะทางที่สั้นที่สุดเท่าที่จะเป็นไปได้เพื่อให้ได้มุมของเมาส์ ปัญหาเกิดขึ้นเมื่อค่าวนรอบที่เส้นแนวนอน ( 3.14และ-3.14) วางเมาส์เหนือบริเวณนั้นเพื่อดูว่าทิศทางเปลี่ยนไปอย่างไรและมันจะย้อนกลับไปได้ไกล

รหัสที่เกี่ยวข้อง

// ease the current angle to the target angle
joint.angle += ( joint.targetAngle - joint.angle ) * 0.1;

// get angle from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;  
joint.targetAngle = Math.atan2( dy, dx );

ฉันจะทำให้มันหมุนระยะทางที่สั้นที่สุดแม้ "ข้ามช่องว่าง" ได้อย่างไร?


ใช้โมดูโล่ : D en.wikipedia.org/wiki/Modular_arithmetic
Vaughan Hilts

1
@VaughanHilts ไม่แน่ใจว่าฉันจะใช้สถานการณ์ของฉันได้อย่างไร คุณสามารถอธิบายเพิ่มเติมได้หรือไม่
jackrugile

คำตอบ:


11

มันไม่ได้เป็นวิธีที่ดีที่สุดเสมอไปและอาจมีค่าใช้จ่ายในการคำนวณที่สูงกว่า (แม้ว่าท้ายที่สุดแล้วขึ้นอยู่กับว่าคุณเก็บข้อมูลของคุณอย่างไร) แต่ฉันจะทำให้อาร์กิวเมนต์ที่การอ่านค่า 2D ทำงานได้ดีในกรณีส่วนใหญ่ แทนที่จะ lerping มุมต้องการคุณสามารถ lerp ที่ต้องการปกติเวกเตอร์ทิศทาง

ข้อดีอย่างหนึ่งของวิธีนี้เหนือวิธี "เลือกเส้นทางที่สั้นที่สุดไปยังมุม" คือการทำงานเมื่อคุณต้องการแทรกระหว่างค่ามากกว่าสองค่า

เมื่อ lerping ค่าสีคุณสามารถแทนที่hueด้วย[cos(hue), sin(hue)]เวกเตอร์

ในกรณีของคุณ lerping ทิศทางร่วมร่วม:

// get normalised direction from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;
var len = Math.sqrt(dx * dx + dy * dy);
dx /= len ? len : 1.0; dy /= len ? len : 1.0;
// get current direction
var dirx = cos(joint.angle),
    diry = sin(joint.angle);
// ease the current direction to the target direction
dirx += (dx - dirx) * 0.1;
diry += (dy - diry) * 0.1;

joint.angle = Math.atan2(diry, dirx);

รหัสสามารถสั้นลงได้ถ้าคุณสามารถใช้คลาสเวกเตอร์ 2D ตัวอย่างเช่น

// get normalised direction from joint to mouse
var desired_dir = normalize(vec2(e.clientX, e.clientY) - joint);
// get current direction
var current_dir = vec2(cos(joint.angle), sin(joint.angle));
// ease the current direction to the target direction
current_dir += (desired_dir - current_dir) * 0.1;

joint.angle = Math.atan2(current_dir.y, current_dir.x);

ขอบคุณนี่ใช้งานได้ดีที่นี่: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204fฉันเข้าใจว่าส่วนใหญ่ของคุณกำลังทำอะไรอยู่ ไม่แน่ใจว่าเกิดอะไรขึ้นที่นั่นdx /= len...
jackrugile

1
หารเวกเตอร์โดยความยาวของมันจะเรียกว่าการฟื้นฟู มันช่วยให้มั่นใจได้ว่ามีความยาว 1 len ? len : 1.0ส่วนเพียงแค่หลีกเลี่ยงการหารด้วยศูนย์ในกรณีที่หายากที่วางเมาส์ตรงตำแหน่งร่วมกัน มันสามารถเขียนได้: if (len != 0) dx /= len;.
sam hocevar

-1 คำตอบนี้อยู่ไกลจากที่ดีที่สุดในกรณีส่วนใหญ่ เกิดอะไรขึ้นถ้าคุณกำลังแก้ไขระหว่างและ180°? ในรูปแบบเวกเตอร์: และ[1, 0] [-1, 0]เวกเตอร์ interpolating จะให้คุณทั้งสอง, 180°หรือหารด้วย 0 t=0.5ข้อผิดพลาดในกรณีของ
Gustavo Maciel

@GustavoMaciel นั่นไม่ใช่“ กรณีส่วนใหญ่” นั่นเป็นกรณีมุมที่เฉพาะเจาะจงมากที่ไม่เคยเกิดขึ้นในทางปฏิบัติ นอกจากนี้ยังไม่มีการหารศูนย์ตรวจสอบรหัส
sam hocevar

@GustavoMaciel มีการตรวจสอบรหัสอีกครั้งจริง ๆ แล้วมันปลอดภัยมากและทำงานได้อย่างที่ควรจะเป็นแม้ในกรณีมุมคุณ
sam hocevar

11

เคล็ดลับคือการจำไว้ว่ามุม (อย่างน้อยในอวกาศยูคลิด) นั้นเป็นระยะ 2 * pi หากความแตกต่างระหว่างมุมปัจจุบันและมุมเป้าหมายมีขนาดใหญ่เกินไป (เช่นเคอร์เซอร์ข้ามเขตแดน) เพียงแค่ปรับมุมปัจจุบันโดยการเพิ่มหรือลบ 2 * pi ตามลำดับ

ในกรณีนี้คุณสามารถลองทำสิ่งต่อไปนี้: (ฉันไม่เคยตั้งโปรแกรมใน Javascript มาก่อนดังนั้นโปรดยกโทษให้สไตล์การเข้ารหัสของฉัน)

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;
  joint.angle += ( joint.targetAngle - joint.angle ) * joint.easing;

แก้ไข : ในการดำเนินการนี้การย้ายเคอร์เซอร์เร็วเกินไปรอบจุดศูนย์กลางของรอยต่อทำให้กระตุก dthetaนี่คือพฤติกรรมที่ตั้งใจไว้ตั้งแต่ความเร็วเชิงมุมของการร่วมทุนอยู่เสมอสัดส่วนกับ หากพฤติกรรมนี้ไม่พึงประสงค์สามารถแก้ไขปัญหาได้ง่าย ๆ โดยวางฝาครอบไว้ที่การเร่งความเร็วเชิงมุมของข้อต่อ

ในการทำเช่นนี้เราจะต้องติดตามความเร็วของข้อต่อและกำหนดอัตราเร่งสูงสุด:

  joint = {
    // snip
    velocity: 0,
    maxAccel: 0.01
  },

จากนั้นเพื่อความสะดวกของเราเราจะแนะนำฟังก์ชั่นการตัด:

function clip(x, min, max) {
  return x < min ? min : x > max ? max : x
}

ตอนนี้รหัสการเคลื่อนไหวของเรามีลักษณะเช่นนี้ อันดับแรกเราคำนวณdthetaเหมือนก่อนปรับjoint.angleตามความจำเป็น:

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;

จากนั้นแทนที่จะเคลื่อนที่ข้อต่อทันทีเราคำนวณความเร็วเป้าหมายและใช้clipบังคับภายในช่วงที่เรายอมรับได้

  var targetVel = ( joint.targetAngle - joint.angle ) * joint.easing;
  joint.velocity = clip(targetVel,
                        joint.velocity - joint.maxAccel,
                        joint.velocity + joint.maxAccel);
  joint.angle += joint.velocity;

สิ่งนี้ทำให้เกิดการเคลื่อนไหวที่ราบรื่นแม้ในขณะเปลี่ยนทิศทางขณะทำการคำนวณในมิติเดียวเท่านั้น นอกจากนี้ยังช่วยให้ความเร็วและความเร่งของข้อต่อสามารถปรับได้อย่างอิสระ ดูตัวอย่างได้ที่นี่: http://codepen.io/anon/pen/HGnDF/


วิธีนี้เข้ามาใกล้จริงๆ แต่ถ้าฉันหนูเร็วเกินไปมันก็เริ่มกระโดดผิดทางเล็กน้อย การสาธิตที่นี่แจ้งให้เราทราบหากฉันไม่ได้ใช้งานอย่างถูกต้อง: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile

1
ฉันไม่แน่ใจว่าคุณหมายถึงอะไรโดยการกระโดดผิดทางเล็กน้อย สำหรับฉันข้อต่อจะเคลื่อนที่ไปในทิศทางที่ถูกต้องเสมอ แน่นอนว่าถ้าคุณเลื่อนเมาส์ไปรอบ ๆ ศูนย์เร็วเกินไปคุณก็จะวิ่งออกมาที่ข้อต่อและกระตุกในขณะที่มันสลับจากการเคลื่อนที่ในทิศทางเดียวไปอีกทิศทางหนึ่ง dthetaนี่คือพฤติกรรมที่ตั้งใจไว้ตั้งแต่ความเร็วในการหมุนอยู่เสมอสัดส่วนกับ คุณตั้งใจจะให้ข้อต่อมีแรงผลักดันบ้างไหม?
David Zhang

ดูการแก้ไขล่าสุดของฉันสำหรับวิธีการกำจัดการเคลื่อนไหวกระตุกที่สร้างขึ้นโดยเอาชนะข้อต่อ
David Zhang

เฮ้ลองลิงค์ตัวอย่างของคุณ แต่ดูเหมือนว่ามันจะชี้ไปที่ลิงก์ดั้งเดิมของฉัน คุณบันทึกใหม่หรือไม่ หากไม่เป็นไรฉันควรจะสามารถนำสิ่งนี้ไปใช้ในคืนนี้และดูว่ามันทำงานอย่างไร ฉันคิดว่าสิ่งที่คุณอธิบายในการแก้ไขเป็นสิ่งที่ฉันกำลังมองหา จะกลับมาหาคุณขอบคุณ!
jackrugile

1
โอ้ใช่คุณพูดถูก ขอโทษที่ฉันทำผิด ฉันจะแก้ไขมัน
David Zhang

3

ฉันรักคำตอบอื่น ๆ ที่ได้รับ ทางเทคนิคมาก!

ถ้าคุณต้องการฉันมีวิธีง่าย ๆ ในการทำสิ่งนี้ เราจะสมมุติมุมสำหรับตัวอย่างเหล่านี้ แนวคิดสามารถคาดการณ์กับค่าชนิดอื่นเช่นสี

double MAX_ANGLE = 360.0;
double startAngle = 300.0;
double endAngle = 15.0;
double distanceForward = 0.0;  // Clockwise
double distanceBackward = 0.0; // Counter-Clockwise

// Calculate both distances, forward and backward:
distanceForward = endAngle - startAngle;    // -285.00
distanceBackward = startAngle - endAngle;   // +285.00

// Which direction is shortest?
// Forward? (normalized to 75)
if (NormalizeAngle(distanceForward) < NormalizeAngle(distanceBackward)) {
    // Adjust for 360/0 degree wrap
    if (endAngle < startAngle) endAngle += MAX_ANGLE; // Will be above 360
}

// Backward? (normalized to 285)
else {
    // Adjust for 360/0 degree wrap
    if (endAngle > startAngle) endAngle -= MAX_ANGLE; // Will be below 0
}

// Now, Lerp between startAngle and endAngle. 

// EndAngle can be above 360 if wrapping clockwise past 0, or
// EndAngle can be below 0 if wrapping counter-clockwise before 0.
// Normalize each returned Lerp value to bring angle in range of 0 to 360 if required.  Most engines will automatically do this for you.


double NormalizeAngle(double angle) {
    while (angle < 0) 
        angle += MAX_ANGLE;
    while (angle >= MAX_ANGLE) 
        angle -= MAX_ANGLE;
    return angle;
}

ฉันเพิ่งสร้างสิ่งนี้ในเบราว์เซอร์และไม่เคยทดสอบ ฉันหวังว่าฉันจะใช้ตรรกะในการลองครั้งแรก

[แก้ไข] 2017/06/02 - ชี้แจงตรรกะเล็กน้อย

เริ่มต้นด้วยการคำนวณ distanceForward และ distanceBackwards และอนุญาตให้ผลลัพธ์ขยายออกไปนอกช่วง (0-360)

การทำให้เป็นปกติของมุมทำให้ค่าเหล่านั้นกลับสู่ช่วง (0-360) ในการทำเช่นนี้คุณต้องเพิ่ม 360 จนกว่าค่าจะสูงกว่าศูนย์และลบ 360 ในขณะที่ค่านั้นสูงกว่า 360 มุมเริ่มต้น / สิ้นสุดที่ได้จะเท่ากับ (-285 เท่ากับ 75)

ถัดไปคุณจะพบมุมที่เล็กที่สุดที่ปรับให้เป็นมาตรฐานของระยะทางไปข้างหน้าหรือระยะทางด้านหลัง distanceForward ในตัวอย่างจะกลายเป็น 75 ซึ่งน้อยกว่าค่าปกติของ distanceBackward (300)

หาก distanceForward มีขนาดเล็กที่สุดและ endAngle <startAngle ให้ขยาย endAngle เกิน 360 โดยเพิ่ม 360 (ตัวอย่างจะกลายเป็น 375 ในตัวอย่าง)

หาก distanceBackward มีขนาดเล็กที่สุดและสิ้นสุดแองเกิล> startAngle ให้ขยาย endAngle เป็นต่ำกว่า 0 โดยการลบ 360

ตอนนี้คุณจะอ่านจาก startAngle (300) ถึง endAngle ใหม่ (375) เครื่องยนต์ควรปรับค่าที่สูงกว่า 360 โดยอัตโนมัติด้วยการลบ 360 สำหรับคุณ มิฉะนั้นคุณจะต้องอ่านค่าจาก 300 ถึง 360 จากนั้นอ่านค่าจาก 0 ถึง 15 หากเครื่องยนต์ไม่ปรับมาตรฐานให้เป็นค่าปกติ


แม้ว่าatan2ความคิดพื้นฐานนั้นจะตรงไปตรงมา แต่แนวคิดนี้สามารถประหยัดพื้นที่และประสิทธิภาพเล็กน้อย (ไม่จำเป็นต้องเก็บ x และ y หรือคำนวณบาป cos และ atan2 บ่อยเกินไป) ฉันคิดว่ามันคุ้มค่าที่จะขยายตัวสักหน่อยว่าทำไมการแก้ปัญหานี้จึงถูกต้อง (เช่นการเลือกเส้นทางที่สั้นที่สุดในวงกลมหรือทรงกลมเช่นเดียวกับ SLERP ที่ใช้สำหรับคำถาม คำถามและคำตอบนี้ควรอยู่ในวิกิชุมชนเพราะนี่เป็นปัญหาที่พบบ่อยมากที่นักเขียนโปรแกรมเกมส่วนใหญ่เผชิญ
teodron

คำตอบที่ดี ฉันชอบคำตอบอื่น ๆ ที่ได้รับจาก @David Zhang อย่างไรก็ตามฉันมักจะได้รับความกระวนกระวายใจแปลก ๆ โดยใช้วิธีการแก้ไขของเขาเมื่อฉันหมุนอย่างรวดเร็ว แต่คำตอบของคุณลงตัวในเกมของฉัน ฉันสนใจในทฤษฎีคณิตศาสตร์ที่อยู่เบื้องหลังคำตอบของคุณ แม้ว่ามันจะดูเรียบง่าย แต่ก็ไม่ชัดเจนว่าทำไมเราควรเปรียบเทียบระยะมุมปกติของทิศทางต่าง ๆ
newguy

ฉันดีใจที่รหัสของฉันทำงาน ดังที่ได้กล่าวมานี่เป็นเพียงแค่พิมพ์ลงในเบราว์เซอร์ไม่ได้ทดสอบในโครงการจริง ฉันจำการตอบคำถามนี้ไม่ได้ด้วยซ้ำ (สามปีก่อน)! แต่เมื่อมองดูมันดูเหมือนว่าฉันเพิ่งขยายช่วง (0-360) เพื่อให้ค่าทดสอบเกินกว่า / ก่อนช่วงนี้เพื่อเปรียบเทียบความแตกต่างขององศาทั้งหมดและใช้ความแตกต่างที่สั้นที่สุด การทำให้เป็นมาตรฐานจะทำให้ค่าเหล่านั้นอยู่ในช่วง (0-360) อีกครั้ง ดังนั้น DistanceForward จึงกลายเป็น 75 (-285 + 360) ซึ่งเล็กกว่าระยะทาง Backward (285) ดังนั้นจึงเป็นระยะทางที่สั้นที่สุด
Doug.McFarlane

เนื่องจาก distanceForward เป็นระยะทางที่สั้นที่สุดส่วนแรกของประโยค IF จะถูกใช้ เนื่องจาก endAngle (15) น้อยกว่า startAngle (300) เราเพิ่ม 360 เพื่อรับ 375 ดังนั้นคุณจะอ่านจาก startAngle (300) ไปที่ endAngle ใหม่ (375) เครื่องยนต์ควรปรับค่าที่สูงกว่า 360 โดยอัตโนมัติด้วยการลบ 360 สำหรับคุณ
Doug.McFarlane

ฉันแก้ไขคำตอบเพื่อเพิ่มความชัดเจนมากขึ้น
Doug.McFarlane
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.