Flutter-Flame:如何计算光线投射的反射?

Flutter-Flame: How to calculate the relect of a raycast?

感谢您的阅读。我的英语不好,我会尽可能地解释这个。 我需要在 canvas 中生成反射线,但效果不佳。

我认为我的问题是“计算反射矢量的公式”。

之前学的数学和canvas不一样,所以很迷茫

这是我的代码(错误),你可以运行这个(第一行是对的,但是反射错了):

import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame_forge2d/body_component.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart';

import 'component/fixture/fixture_data.dart';

void main() {
  runApp(
    GameWidget(
      game: TestGame(),
    ),
  );
}

class TestGame extends Forge2DGame with MultiTouchDragDetector {
  List<Offset> points = [Offset.zero, Offset.zero, Offset.zero];
  late var boundaries;

  @override
  Future<void> onLoad() async {
    await super.onLoad();

    boundaries = createBoundaries(this);
    boundaries.forEach(add);

    points[0] = (camera.canvasSize / 2).toOffset();
  }

  @override
  void update(double dt) {
    super.update(dt);
  }

  @override
  void render(Canvas canvas) {
    super.render(canvas);

    canvas.drawLine(points[0], points[1], Paint()..color = Colors.white);

    canvas.drawLine(points[1], points[2], Paint()..color = Colors.white);

    canvas.drawCircle(points[2], 30, Paint()..color = Colors.white);
  }

  /// @param [pointStart] start point of line
  /// @param [point2] second point of line
  /// @param [x] the position x of point need to calculate
  /// @return Offset of the point on line
  Offset calculateOffsetByX(Offset pointStart, Offset point2, double x) {
    //y =  ax + b
    final a = (pointStart.dy - point2.dy) / (pointStart.dx - point2.dx);
    final b = pointStart.dy - a * pointStart.dx;
    return Offset(x, a * x + b);
  }

  /// @param [pointStart] start point of line
  /// @param [point2] second point of line
  /// @param [y] the position y of point need to calculate
  /// @return Offset of the point on line
  Offset calculateOffsetByY(Offset pointStart, Offset point2, double y) {
    //y =  ax + b
    final a = (pointStart.dy - point2.dy) / (pointStart.dx - point2.dx);
    final b = pointStart.dy - a * pointStart.dx;
    return Offset((y - b) / a, y);
  }

  @override
  void onDragUpdate(int pointerId, DragUpdateInfo info) {
    var callback = MyRayCastCallBack(this);

    var finalPos = getFinalPos(screenToWorld(camera.viewport.effectiveSize) / 2,
        info.eventPosition.game);

    world.raycast(
        callback, screenToWorld(camera.viewport.effectiveSize) / 2, finalPos);

    var n = callback.normal;
    var i = info.eventPosition.game;
    var r = i + (n * (2 * i.dot(n)));

    var callback2 = MyRayCastCallBack2(this);
    world.raycast(callback2, callback.point, r);
  }

  Vector2 getFinalPos(Vector2 startPos, Vector2 touchPos) {
    return Vector2(
        calculateOffsetByY(startPos.toOffset(), touchPos.toOffset(), 0).dx,
        calculateOffsetByY(startPos.toOffset(), touchPos.toOffset(), 0).dy);
  }
}

class MyRayCastCallBack extends RayCastCallback {
  TestGame game;
  late Vector2 normal;
  late Vector2 point;

  MyRayCastCallBack(this.game);

  @override
  double reportFixture(
      Fixture fixture, Vector2 point, Vector2 normal, double fraction) {
    game.points[1] = game.worldToScreen(point).toOffset();
    this.normal = normal;
    this.point = point;
    return 0;
  }
}

class MyRayCastCallBack2 extends RayCastCallback {
  TestGame game;
  late Vector2 normal;
  late Vector2 point;

  MyRayCastCallBack2(this.game);

  @override
  double reportFixture(
      Fixture fixture, Vector2 point, Vector2 normal, double fraction) {
    game.points[2] = game.worldToScreen(point).toOffset();
    this.normal = normal;
    this.point = point;
    return 0;
  }
}

List<Wall> createBoundaries(Forge2DGame game) {
  /*final topLeft = Vector2.zero();
  final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize);
  final topRight = Vector2(bottomRight.x, topLeft.y);
  final bottomLeft = Vector2(topLeft.x, bottomRight.y);*/
  final bottomRight =
      game.screenToWorld(game.camera.viewport.effectiveSize) / 8 * 7;

  final topLeft = game.screenToWorld(game.camera.viewport.effectiveSize) / 8;
  final topRight = Vector2(bottomRight.x, topLeft.y);
  final bottomLeft = Vector2(topLeft.x, bottomRight.y);

  return [
    Wall(topLeft, topRight, FixtureKey.wallTop),
    Wall(topRight, bottomRight, FixtureKey.wallRight),
    Wall(bottomRight, bottomLeft, FixtureKey.wallBottom),
    Wall(bottomLeft, topLeft, FixtureKey.wallLeft),
  ];
}

class Wall extends BodyComponent {
  final Vector2 start;
  final Vector2 end;
  final FixtureKey fixtureKey;

  Wall(this.start, this.end, this.fixtureKey);

  @override
  Body createBody() {
    final shape = EdgeShape()..set(start, end);

    final fixtureDef = FixtureDef(shape)
      ..restitution = 0
      ..density = 1.0
      ..friction = 0
      ..userData = FixtureData(type: FixtureType.wall, key: fixtureKey);
    ;

    final bodyDef = BodyDef()
      ..userData = this // To be able to determine object in collision
      ..position = Vector2.zero()
      ..type = BodyType.static;

    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}


您的代码中有几处错误。但是您的猜测是正确的,反映关于法线的光线投射的数学是错误的。更准确地说,您用作第一个光线投射的矢量似乎是错误的。

而不是,

var i = info.eventPosition.game;

应该是这样的,

var i = callback.point - (screenToWorld(camera.viewport.effectiveSize) / 2);

基本上,它应该是一个相对向量,从你原来的起点开始,指向命中点。希望这能解决您的问题。

您的代码中还有些不对劲的地方

  • 多重回调classes:您不需要定义多重回调classes来捕获多重光线投射。您可以使用相同 class 的多个对象。如果你真的想推送它,你甚至可以根据你的用例一次又一次地使用同一个对象。

  • Return 来自 reportFixture():我看到你是从这个方法 returning 0。在您的简单情况下,这可能会很好地工作,但是 returning 0 意味着您希望光线投射在它遇到的第一个固定装置处停止。不幸的是,world.raycast does not report fixtures in any fixed order。因此,如果沿射线有多个固定装置,则 reportFixture 可能会被调用以获得最远的固定装置。如果你真的想获得最近的灯具,你应该 return fraction 参数作为 return.

我在这里发布了一个示例代码,它可以根据 nBounces 参数向您报告多次点击。

void newRaycast(Vector2 start, Vector2 end, int nBounces) {
  if (nBounces > 0) {
    final callback = NearestRayCastCallback();
    world.raycast(callback, start, end);
    
    // Make sure that we got a hit.
    if (callback.nearestPoint != null && callback.normalAtInter != null) {
      // Store the hit location for rendering later on.
      points.add(worldToScreen(callback.nearestPoint!.clone()));

      // Figure out the current ray direction and then reflect it
      // about the collision normal.
      Vector2 originalDirection =
          (callback.nearestPoint! - start).normalized();
      final dotProduct = callback.normalAtInter!.dot(originalDirection);
      final newDirection =
          originalDirection - callback.normalAtInter!.scaled(2 * dotProduct);

      // Call newRayCast with new start and end points. New end is just a point 500
      // units along the new direction.
      newRaycast(
          callback.nearestPoint!.clone(), newDirection.scaled(500), --nBounces);
    }
  }
}

这是我的回调的定义 class。

class NearestRayCastCallback extends RayCastCallback {
    Vector2? nearestPoint;
    Vector2? normalAtInter;

    @override
    double reportFixture(
        Fixture fixture, Vector2 point, Vector2 normal, double fraction) {
    nearestPoint = point;
    normalAtInter = normal;
    return fraction;
    }
}

在上面的代码中,pointsfinal points = List<Vector2>.empty(growable: true);,这就是我从这些点渲染线的方式:

for (int i = 0; i < points.length - 1; ++i) {
  canvas.drawLine(points[i].toOffset(), points[i + 1].toOffset(),
      Paint()..color = Colors.black..strokeWidth = 2);
}