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;
}
}
在上面的代码中,points
是 final 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);
}
感谢您的阅读。我的英语不好,我会尽可能地解释这个。 我需要在 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
可能会被调用以获得最远的固定装置。如果你真的想获得最近的灯具,你应该 returnfraction
参数作为 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;
}
}
在上面的代码中,points
是 final 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);
}