Created
February 7, 2020 05:12
-
-
Save stargazing-dino/67037d98e1bd3a3c3abed5da5142e14c to your computer and use it in GitHub Desktop.
A simple terrain generation algorithm implemented in flutter based off https://www.youtube.com/watch?v=IKB1hWWedMk&list=PLRqwX-V7Uu6ZiZxtDDRCi6uhfTH4FilpH&index=14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math'; | |
import 'dart:typed_data'; | |
import 'dart:ui' as UI; | |
import 'package:flutter/material.dart'; | |
import 'package:vector_math/vector_math_64.dart' hide Colors; | |
final TargetPlatform platform = TargetPlatform.android; | |
void main() { | |
runApp(App()); | |
} | |
void main() { | |
runApp(App()); | |
} | |
class App extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData.dark(), | |
debugShowCheckedModeBanner: false, | |
home: TerrainGeneration(), | |
); | |
} | |
} | |
final up = Vector3(0.0, 1.0, 0.0); | |
class TerrainGeneration extends StatefulWidget { | |
@override | |
TerrainGenerationState createState() => TerrainGenerationState(); | |
} | |
class TerrainGenerationState extends State<TerrainGeneration> | |
with SingleTickerProviderStateMixin { | |
// Fields private to this class | |
AnimationController _animationController; | |
final _customPaintKey = GlobalKey(); | |
bool _isInitialized = false; | |
// Accessible in TerrainGenerationPainter. | |
// There is no need to setState after updating these | |
// They'll update with every vsync tick. | |
int columns, rows; | |
List<List<double>> terrain; | |
double scale = 40.0; | |
var flying = 0.0; | |
final simplexNoise = SimplexNoise(); | |
@override | |
void initState() { | |
_animationController = AnimationController( | |
vsync: this, | |
duration: const Duration(days: 365), | |
); | |
// Await the build phase to be complete in order to get sizing | |
Future.delayed(Duration.zero, () { | |
final context = _customPaintKey.currentContext; | |
final RenderBox box = context.findRenderObject(); | |
setup(box.size); | |
}); | |
_animationController.forward(); | |
super.initState(); | |
} | |
@override | |
void dispose() { | |
_animationController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(title: Text('Waves')), | |
body: SizedBox.expand( | |
child: AnimatedBuilder( | |
animation: _animationController, | |
builder: (context, __) { | |
return CustomPaint( | |
key: _customPaintKey, | |
willChange: true, | |
painter: _isInitialized | |
? TerrainGenerationPainter(_animationController, this) | |
: null, | |
); | |
}, | |
), | |
), | |
); | |
} | |
void setup(Size size) { | |
final width = size.width; | |
// Add extra columns to help it fill the width of the screen | |
columns = (width / scale).round() + 16; | |
rows = (size.height / scale).round(); | |
terrain = List.generate( | |
rows, | |
(_) => List.generate(columns + 1, (_) => 0.0), | |
); | |
setState(() => _isInitialized = true); | |
} | |
} | |
class TerrainGenerationPainter extends CustomPainter { | |
const TerrainGenerationPainter(this.animation, this.state) | |
: super(repaint: animation); | |
final Animation<double> animation; | |
final TerrainGenerationState state; | |
@override | |
void paint(Canvas canvas, Size size) { | |
final scale = state.scale; | |
state.flying -= 0.01; | |
var iOff = state.flying; | |
for (int i = 0; i < state.rows; i++) { | |
var jOff = 0.0; | |
state.terrain.add([]); | |
for (int j = 0; j < state.columns; j++) { | |
state.terrain[i][j] = state.simplexNoise.noise2D(iOff, jOff) * .25; | |
// terrain[i][j] = random.nextDouble() * .25; | |
jOff += 0.1; | |
} | |
iOff += 0.1; | |
} | |
final List<List<Vector3>> allVertices = []; | |
// final List<List<Vector3>> allVertices = []; | |
// To Account for all those columns I added in order to fill the space | |
canvas.translate(-100, 0); | |
for (int i = 0; i < state.rows; i++) { | |
allVertices.add([]); | |
for (int j = 0; j < state.columns - 1; j++) { | |
final vertices = allVertices[i]; | |
vertices.add( | |
Vector3(j * scale, i * scale, state.terrain[i][j]), | |
); | |
vertices.add( | |
Vector3(j * scale, (i + 1) * scale, state.terrain[i][j + 1]), | |
); | |
} | |
} | |
final width = size.width; | |
final height = size.height; | |
final cameraPosition = Vector3(0, 0, 6); | |
final brush = Paint() | |
..color = Colors.red | |
..style = PaintingStyle.stroke | |
..strokeWidth = 4.0; | |
final lookAt = Vector3.zero(); | |
final cameraDistance = cameraPosition.distanceTo(lookAt); | |
final aspectRatio = width / height; | |
final zNearPlane = cameraDistance - 0.1; | |
final zFarPlane = cameraDistance * 2; | |
final fovYRadians = 0.7; | |
// Build matrices to get us into an OpenGL-like coordinate space | |
// bottom left corner of the screen is at (-1, -1), top right is at (1, 1) | |
final toOpenGlCoords = Matrix4.compose( | |
Vector3(-1.0, 1.0, 0.0), | |
Quaternion.axisAngle(Vector3(1.0, 0.0, 0.0), pi), | |
Vector3(2 / width, 2 / height, 1.0), | |
); | |
final toFlutterCoords = Matrix4.tryInvert(toOpenGlCoords); | |
var modelMatrix = Matrix4.rotationX(-pi / 2.3).scaled(1.0, 4.5, 1.0); | |
final viewMatrix = makeViewMatrix(cameraPosition, lookAt, up); | |
final projectionMatrix = makePerspectiveMatrix( | |
fovYRadians, | |
aspectRatio, | |
zNearPlane, | |
zFarPlane, | |
); | |
final mvp = projectionMatrix * viewMatrix * modelMatrix; | |
final flutterCompatTransform = toFlutterCoords * mvp * toOpenGlCoords; | |
for (final vertices in allVertices) { | |
final projected = vertices.map<Vector2>((vertex) { | |
final vertexCopy = vertex.clone(); | |
vertexCopy.applyProjection(flutterCompatTransform); | |
return Vector2(vertexCopy.x, vertexCopy.y); | |
}).toList(); | |
final storage = _encodeVector2List(projected); | |
canvas.drawVertices( | |
UI.Vertices.raw( | |
VertexMode.triangleStrip, | |
storage, | |
), | |
BlendMode.srcATop, | |
brush, | |
); | |
} | |
} | |
// This is copied from dart:ui | |
Int32List _encodeColorList(List<Color> colors) { | |
final int colorCount = colors.length; | |
final Int32List result = Int32List(colorCount); | |
for (int i = 0; i < colorCount; ++i) result[i] = colors[i].value; | |
return result; | |
} | |
// This is copied from dart:ui | |
Float32List _encodeVector2List(List<Vector2> vertices) { | |
final int pointCount = vertices.length; | |
final Float32List result = Float32List(pointCount * 2); | |
for (int i = 0; i < pointCount; ++i) { | |
final int xIndex = i * 2; | |
final int yIndex = xIndex + 1; | |
final Vector2 point = vertices[i]; | |
result[xIndex] = point.x; | |
result[yIndex] = point.y; | |
} | |
return result; | |
} | |
@override | |
bool shouldRepaint(CustomPainter oldDelegate) => true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment