This commit is contained in:
Miloslav Ciz 2022-06-29 20:56:10 +02:00
parent c856794e38
commit eca7446b98
8 changed files with 279 additions and 40 deletions

View file

@ -2,7 +2,7 @@
In [computer graphics](graphics.md) 3D rendering is concerned with computing images that represent a projected view of 3D objects through a virtual camera. There are many methods and [algorithms](algorithm.md) for doing so differing in many aspects such as computation complexity, implementation complexity, realism of the result, representation of the 3D data, limitations of viewing and so on.
A table of some common rendering methods, including the most simple and most advanced ones, follows. Note that here we talk about methods rather than algorithms, i.e. general approaches that are often modified and combined into a specific rendering algorithm. The table should help you craft an ideal 3D rendering algorithm for your program.
A table of some common rendering methods follows, including the most simple, most advanced and some unconventional ones. Note that here we talk about methods and techniques rather than algorithms, i.e. general approaches that are often modified and combined into a specific rendering algorithm. For example the traditional triangle rasterization is sometimes combined with raytracing to add e.g. realistic reflections. The methods may also be further enriched with features such as [texturing](texture.md), [antialiasing](antialiasing.md) and so on. The table below should help you choose the base 3D rendering method for your specific program.
The methods may be tagged with the following:
@ -21,14 +21,15 @@ The methods may be tagged with the following:
|"[dungeon crawler](dungeon_crawler.md)" |*OO 2.5D*, e.g. Eye of the Beholder |
|ellipsoid rasterization |*OO*, e.g. Ecstatica |
|flat-shaded 1 point perspective |*OO 2.5D*, e.g. Skyroads |
|[image based rendering](ibr.md) | |
|reverse raytracing (photon tracing) |*OO off*, inefficient |
|[image based rendering](ibr.md) |generating inbetween views |
|[mode 7](mode7.md) |*IO 2.5D*, e.g. F-Zero |
|[parallax scrolling](parallax.md) |*2.5D*, very primitive |
|[pathtracing](pathtracing.md) |*IO off*, Monte Carlo, high realism |
|[portal rendering](portal_rendering.md) |*2.5D*, e.g. [Duke3D](duke3d.md) |
|prerendered view angles |*2.5D* |
|[raymarching](raymaching.md) |*IO off* |
|[raytracing](raytracing.md) |*IO off*, recursive raycasting |
|prerendered view angles |*2.5D*, e.g. Iridion II (GBA) |
|[raymarching](raymaching.md) |*IO off*, e.g. with [SDFs](sdf.md) |
|[raytracing](raytracing.md) |*IO off*, recursive 3D raycasting |
|segmented road |*OO 2.5D*, e.g. Outrun |
|[shear warp rednering](shear_warp.md) |*IO*, volumetric |
|[splatting](splatting.md) |*OO*, rendering with 2D blobs |
@ -36,6 +37,8 @@ The methods may be tagged with the following:
|[voxel space rendering](voxel_space.md) |*OO 2.5D*, e.g. Comanche |
|[wireframe rendering](wireframe.md) |*OO*, just lines |
TODO: Rescue On Fractalus!
TODO: find out how build engine/slab6 voxel rendering worked and possibly add it here (from http://advsys.net/ken/voxlap.htm seems to be based on raycasting)
TODO: VoxelQuest has some innovative voxel rendering, check it out (https://www.voxelquest.com/news/how-does-voxel-quest-work-now-august-2015-update)

View file

@ -1,6 +1,8 @@
# Algorithm
Algorithm is an exact description of how to solve a problem. Algorithms are basically what [programming](programming.md) is all about: we tell computers, in very exact ways (with [programming languages](programming_language.md)), how to solve problems -- we write algorithms. But algorithms don't have to be just computer programs, they are simply instruction for solving problems. (Cooking recipes are sometimes given as an example of a non-computer algorithm. You may write an algorithm for how to survive in a jungle, but it has to be **exact**; if there is an ambiguity or incompleteness, it is not an algorithm.)
Algorithm is an exact description of how to solve a problem. Algorithms are basically what [programming](programming.md) is all about: we tell computers, in very exact ways (with [programming languages](programming_language.md)), how to solve problems -- we write algorithms. But algorithms don't have to be just computer programs, they are simply instruction for solving problems.
Cooking recipes are sometimes given as an example of a non-computer algorithm. The so called wall-follower is a simple algorithm to get out of any maze: you just pick either a left-hand or right-hand wall and then keep following it. You may write a crazy algorithm for how to survive in a jungle, but it has to be **exact**; if there is any ambiguity, it is not considered an algorithm.
Interesting fact: contrary to intuition there are problems that are mathematically proven to be unsolvable by any algorithm, see [undecidability](undecidability.md), but for most practically encountered problems we can write an algorithm (though for some problems even our best algorithms can be unusably slow).

View file

@ -1,6 +1,6 @@
# Billboard
In [3D](3d.md) [computer graphics](graphics.md) billboard is a flat image placed in the scene that rotates so that it's always facing the camera. Billboards used to be greatly utilized instead of actual [3D models](3d_model.md) in old [games](game.md) thanks to being faster to render (and possibly also easier to create than full 3D models), but we can still encounter them even today and even outside retro games, e.g. [particle systems](particle_system.md) are normally rendered with billboards. Billboards are also commonly called *[sprites](sprite.md)*, even though that's not exactly accurate.
In [3D](3d.md) [computer graphics](graphics.md) billboard is a flat image placed in the scene that rotates so that it's always facing the camera. Billboards used to be greatly utilized instead of actual [3D models](3d_model.md) in old [games](game.md) thanks to being faster to render (and possibly also easier to create than full 3D models), but we can still encounter them even today and even outside retro games, e.g. [particle systems](particle_system.md) are normally rendered with billboards (each particle is one billboard). Billboards are also commonly called *[sprites](sprite.md)*, even though that's not exactly accurate.
There are two main types of billboards:

15
game.md
View file

@ -24,6 +24,20 @@ Some games are pretty based as they don't even require [GUI](gui.md) and are onl
Another kind of cool games are computer implementations of pre-computer games, for example [chess](chess.md), backgammon, go or various card games. Such games are very often well tested and fine-tuned gameplay-wise, popular with active communities and therefore [fun](fun.md), yet simple to program with many existing free implementations and good AIs (e.g. GNU chess, GNU go or [Stockfish](stockfish.md)).
## Games As LRS
Games can be [suckless](suckless.md) and just as any other software should try to adhere to the [Unix philosophy](unix_philosophy.md). A [LRS](lrs.md) game should follow all the principles that apply to any other kind of such software, for example being completely [public domain](public_domain.md) or aiming for high [portability](portability.md). This is important to mention because, sadly, many people see games as some kind of exception among software and think that different technological or moral rules apply -- this is wrong.
If you want to make a simple LRS game, there is an official LRS [C](c.md) library for it: [SAF](saf.md).
Compared to mainstream games, a LRS game shouldn't be a consumerist product, it should be a tool to help people entertain themselves and relieve their stress. From the user perspective, the game should be focused on the fun and relaxation aspect rather than on pleasing look, i.e. it will likely utilize simple graphics and audio. Another aspect of an LRS game is that the technological part is just as important as how the game behaves on the outside (unlike mainstream games that have ugly, badly designed internals and mostly focus on impressing the consumer).
The paradigm of LRS gamedev differs from the mainstream gamedev just as the Unix philosophy differs from the Window philosophy. While a mainstream game is a monolithic piece of software, designed to allow at most some simple user modifications, a LRS game is designed with [forking](fork.md) and code reuse in mind.
Let's take an example. A LRS game of a real-time 3D [RPG](rpg.md) genre may for example consist of several independent modules: the RPG library, the game code, the content and the [frontend](frontend.md). Yes, a mainstream game will consist of similar modules, however those modules will probably only exist for the organization of work and better testing, they won't be intended for real reuse or wild hacking. With the LRS RPG game it is implicitly assumed that someone else may take the 3D game and make it into a purely non-real-time [command line](cli.md) game just by replacing the frontend, in which case the rest of the code shouldn't be burdened by anything 3D-rendering related. The paradigm here should be similar to that existing in the world of computer [chess](chess.md) where there exist separate engines, graphical frontends, communication protocols, formats, software for running engine tournaments, analyzing games etc. [Roguelikes](roguelike.md) and the world of [quake](quake.md) engines show some of this modularity, though not in such a degree we would like to see -- LRS game modules may be completely separate projects and different processes communicating via text interfaces through [pipes](pipe.md), just as basic Unix tools do. We have to think about someone possibly taking our singleplayer RPG and make it into an MMORPG. Someone may even take the game and use it as a research tool for [machine learning](machine_learning.md), and the game should be designed so as to make this as easy as possible -- the user interface should be very simple to be replaced by an [API](api.md) for computers. The game should allow easy creation of [tool assisted speedruns](tas.md), to record demos, to allow [scripting](script.md), modifying ingame variables, even creating [cheats](cheat.md) etc. And, importantly, **the game content is a module as well**, i.e. the whole RPG world, its lore and storyline is something that can be modified, forked, remixed, and the game creator should bear this in mind.
Of course, LRS games must NOT contain such shit as "[anti-cheating](anti_cheat.md) technology". For our stance on cheating, see the article [about it](cheat.md).
## Legal Matters
Thankfully gameplay mechanisms cannot (yet) be [copyrighted](copyright.md) (however some can sadly be [patented](patent.md)) so we can mostly happily [clone](clone.md) proprietary games and so free them. However this must be done carefully as there is a possibility of stepping on other mines, for example violating a [*trade dress*](trade_dress.md) (looking too similar visually) or a [trade mark](trade_mark.md) (for example you cannot use the word *tetris* as it's owned by some shitty company) and also said patents (for example the concept of minigames on loading screens used to be patented in the past).
@ -41,4 +55,5 @@ Trademarks have been known to cause problems in the realm of libre games, for ex
- [minigame](minigame.md)
- [open console](open_console.md)
- [fantasy console](fantasy_console.md)
- [SAF](saf.md)
- [chess](chess.md)

5
piracy.md Normal file
View file

@ -0,0 +1,5 @@
# Piracy
Piracy is a capitalist propaganda term for the act of illegally sharing copyrighted information such as [non-free](proprietary.md) books, movies, music, video [games](game.md) or scientific papers.
**[We](lrs.md) highly support piracy**, however also keep in mind the following: if you pirate a proprietary piece of information, you get it gratis but it stays proprietary, it abuses you, it limits your freedom -- you won't get the source code, you won't be able to publicly host it, you won't be allowed to create and share derivative works etc. Therefore **prefer [free (as in freedom)](free_culture.md) alternatives to piracy**, i.e. pieces of information that are not only gratis but also freedom supporting.

View file

@ -4,6 +4,8 @@
Races of people are very large, loosely defined groups of genetically similar people. Races differ by their look and in physical, mental and cultural aspects. This topic is nowadays forbidden to be discussed and researched in "[science](soyence.md)", however there exists a number of older research and some things are just obvious.
Most generally races are called by the color of their skin, i.e. White (Caucasian), Black (African), Yellow (Asian) and Brown (Indian). But the lines can be drawn in many ways, some go as far as calling different nations separate races (e.g. the Norwegian race, Russian race etc.).
There is a controversial 1994 book called *The Bell Curve* that deals with differences between races. An online resource with a lot of information is e.g. http://www.humanbiologicaldiversity.com/.
In relation to [technology](tech.md)/[math](math.md)/[science](science.md) it is useful to know the differences in intellect between different races, though cultural and other traits linked to races may also play a big role. It is important to keep in mind intelligence isn't one dimensional, it's one of the most complex and complicated concepts we can be dealing with (remember the famous Chimp test that revealed that chimpanzees greatly outperform humans at certain intellectual tasks). We can't simplify to a single measure such as [IQ](iq.md). Let intelligence here mean simply the ability to perform well in the area of our art. And of course, there are smart and stupid people in any race, the general statements we make are just about statistics and probabilities.

View file

@ -12,39 +12,40 @@ In [computer graphics](graphics.md) raycasting refers to a rendering technique i
2D raycasting can be used to relatively easily render "3Dish" looking environments (commonly labeled "[pseudo 3D](pseudo3D.md)"), mostly some kind of right-angled labyrinth. There are limitations such as the inability for the camera to tilt up and down (which can nevertheless be faked with shearing). It used to be popular in very old games but can still be used nowadays for "retro" looking games, games for very weak hardware (e.g. [embedded](embedded.md)), in [demos](demoscene.md) etc. It is pretty cool, very [suckless](suckless.md) rendering method.
```
................................................................................
/////////.......................................................................
///////////////.................................................................
//////////////////////..........................................................
//////////////////////..........................................................
//////////////////////..........................................................
//////////////////////..........................................................
//////////////////////..............................X//.........................
//////////////////////...........................XXXX//////////............XXXXX
//////////////////////.......................XXXXXXXX////////////////XXXXXXXXXXX
//////////////////////.......................XXXXXXXX////////////////XXXXXXXXXXX
/////////////////////////////////////////....XXXXXXXX////////////////XXXXXXXXXXX
/////////////////////////////////////////////XXXXXXXX////////////////XXXXXXXXXXX
/////////////////////////////////////////////XXXXXXXX////////////////XXXXXXXXXXX
/////////////////////////////////////////////XXXXXXXX////////////////XXXXXXXXXXX
/////////////////////////////////////////////XXXXXXXX////////////////XXXXXXXXXXX
/////////////////////////////////////////////XXXXXXXX////////////////XXXXXXXXXXX
/////////////////////////////////////////....XXXXXXXX////////////////XXXXXXXXXXX
//////////////////////.......................XXXXXXXX////////////////XXXXXXXXXXX
//////////////////////.......................XXXXXXXX////////////////XXXXXXXXXXX
//////////////////////...........................XXXX//////////............XXXXX
//////////////////////..............................X//.........................
//////////////////////..........................................................
//////////////////////..........................................................
//////////////////////..........................................................
//////////////////////..........................................................
///////////////.................................................................
/////////.......................................................................
................................................................................
................................................................................
....................................................................................................
....................................................................................................
###.................................................................................................
#########...........................................................................................
#########...........................................................................................
#########...........................................................................................
#########...........................................................................................
#########.................///######..................................../#...........................
#########..............//////############.....................//////////###.........................
#########..............//////############............///////////////////####............////////////
#########......///#####//////############.........//////////////////////####////////////////////////
###############///#####//////############.........//////////////////////####////////////////////////
###############///#####//////############//#####////////////////////////####////////////////////////
###############///#####//////############//#####////////////////////////####////////////////////////
###############///#####//////############//#####////////////////////////####////////////////////////
###############///#####//////############//#####////////////////////////####////////////////////////
###############///#####//////############//#####////////////////////////####////////////////////////
###############///#####//////############//#####////////////////////////####////////////////////////
###############///#####//////############//#####////////////////////////####////////////////////////
###############///#####//////############.........//////////////////////####////////////////////////
#########......///#####//////############.........//////////////////////####////////////////////////
#########..............//////############............///////////////////####............////////////
#########..............//////############.....................//////////###.........................
#########.................///######..................................../#...........................
#########...........................................................................................
#########...........................................................................................
#########...........................................................................................
#########...........................................................................................
###.................................................................................................
....................................................................................................
....................................................................................................
```
*image rendered with [raycastlib](raycastlib.md)*
*raycasted view, rendered by the example below*
The method is called *2D* because even though the rendered picture looks like a 3D view, the representation of the world we are rendering is 2 dimensional (usually a grid, a top-down plan of the environment with cells of either empty space or walls) and the casting of the rays is performed in this 2D space -- unlike with the 3D raycasting which really does cast rays in fully 3D environments. Also unlike with the 3D version which casts one ray per each rendered pixel (x * y rays per frame), 2D raycasting only casts **one ray per rendered column** (x rays per frame) which actually, compared to the 3D version, drastically reduces the number of rays cast and makes this method **fast enough for [real time](real_time.md)** rendering even using [software_rendering](sw_rendering.md) (without a GPU).
@ -73,4 +74,215 @@ The core element to implement is the code for casting rays, i.e. given the squar
In the above *V* is the position of the camera (viewer) which is facing towards the point *I*, *p* is the camera plane perpendicular to *VI* at the distance 1 from *V*. Ray *r* is cast from the camera and hits the point *X*. The length of the line *r* is the Euclidean distance, however we want to find out the distance *JX = VI*, which is perpendicular to *p*. There are two similar triangles: *VCA* and *VIB*; from this it follows that *1 / VA = VI / VB*, from which we derive that *JX = VB / VA*. We can therefore calculate the perpendicular distance just from the ratio of the distances along one principal axis (X or Y). However watch out for the case when *VA = VB = 0* to not divide by zero! In such case use the other principal axis (Y).
TODO: code
Here is a complete **[C](c.md) example** that uses only [fixed point](fixed_point.md) with the exception of the stdlib [sin](sin.md)/[cos](cos.md) functions, for simplicity's sake (these can easily be replaced by custom fixed point implementation):
```
#include <stdio.h>
#include <math.h> // for simplicity we'll use float sin, cos from stdlib
#define U 1024 // fixed-point unit
#define LEVEL_SIZE 16 // level resolution
#define SCREEN_W 100
#define SCREEN_H 31
int wallHeight[SCREEN_W];
int wallDir[SCREEN_W];
int perspective(int distance)
{
if (distance <= 0)
distance = 1;
return (SCREEN_H * U) / distance;
}
unsigned char level[LEVEL_SIZE * LEVEL_SIZE] =
{
#define E 1, // wall
#define l 0, // floor
l l l l E l l l l l l l l l E E
l E l l E E E l l l l l E l l E
l l l l l l l l l l l l l l l l
l E l l E l E l E l E l E l l l
l l l l E l l l l l l l l l E l
l l l l E l l l l l l l l l E l
l E E l E l l l l l l l l l l l
l E E l E l l l l l l l l l l l
l E l l l l l l l l l l l l l E
l E l l E l l l l l l l l E l l
l E l l E l l l l l l l l E l l
l E l l l l E E E l l l l l l l
l E E l E l l l l l E E E l l E
l E E l E l l l l l E l l l E E
l l l l l l E E E E E l l E E E
l l E l l l l l l l l l E E E E
#undef E
#undef l
};
unsigned char getTile(int x, int y)
{
if (x < 0 || y < 0 || x >= LEVEL_SIZE || y >= LEVEL_SIZE)
return 1;
return level[y * LEVEL_SIZE + x];
}
// returns perpend. distance to hit and wall direction (0 or 1) in dir
int castRay(int rayX, int rayY, int rayDx, int rayDy, int *dir)
{
int tileX = rayX / U,
tileY = rayY / U,
addX = 1, addY = 1;
// we'll convert all cases to tracing in +x, +y direction
*dir = 0;
if (rayDx == 0)
rayDx = 1;
else if (rayDx < 0)
{
rayDx *= -1;
addX = -1;
rayX = (tileX + 1) * U - rayX % U;
}
if (rayDy == 0)
rayDy = 1;
else if (rayDy < 0)
{
rayDy *= -1;
addY = -1;
rayY = (tileY + 1) * U - rayY % U;
}
int origX = rayX,
origY = rayY;
for (int i = 0; i < 20; ++i) // trace at most 20 squares
{
int px = rayX % U, // x pos. within current square
py = rayY % U,
tmp;
if (py > ((rayDy * (px - U)) / rayDx) + U)
{
tileY += addY; // step up
rayY = ((rayY / U) + 1) * U;
tmp = rayX / U;
rayX += (rayDx * (U - py)) / rayDy;
if (rayX / U != tmp) // don't cross the border due to round. error
rayX = (tmp + 1) * U - 1;
*dir = 0;
}
else
{
tileX += addX; // step right
rayX = ((rayX / U) + 1) * U;
tmp = rayY / U;
rayY += (rayDy * (U - px)) / rayDx;
if (rayY / U != tmp)
rayY = (tmp + 1) * U - 1;
*dir = 1;
}
if (getTile(tileX,tileY)) // hit?
{
px = rayX - origX;
py = rayY - origY;
// get the perpend dist. to camera plane:
return (px > py) ? ((px * U) / rayDx) : ((py * U) / rayDy);
// the following would give the fish eye effect instead
// return sqrt(px * px + py * py);
}
}
return 100 * U; // no hit found
}
void drawScreen(void)
{
for (int y = 0; y < SCREEN_H; ++y)
{
int lineY = y - SCREEN_H / 2;
lineY = lineY >= 0 ? lineY : (-1 * lineY);
for (int x = 0; x < SCREEN_W; ++x)
putchar((lineY >= wallHeight[x]) ? '.' : (wallDir[x] ? '/' : '#'));
putchar('\n');
}
}
int main(void)
{
int camX = 10 * U + U / 4,
camY = 9 * U + U / 2,
camAngle = 600, // U => full angle (2 * pi)
quit = 0;
while (!quit)
{
int forwX = cos((2 * 3.14 * camAngle) / ((float) U) ) * U,
forwY = sin((2 * 3.14 * camAngle) / ((float) U) ) * U,
vecFromX = forwX + forwY, // leftmost ray
vecFromY = forwY - forwX,
vecToX = forwX - forwY, // rightmost ray
vecToY = forwY + forwX;
for (int i = 0; i < SCREEN_W; ++i) // process each screen column
{
// interpolate rays between vecFrom and vecTo
int rayDx = (SCREEN_W - 1 - i) * vecFromX / SCREEN_W + (vecToX * i) / SCREEN_W,
rayDy = (SCREEN_W - 1 - i) * vecFromY / SCREEN_W + (vecToY * i) / SCREEN_W,
dir,
dist = castRay(camX,camY,rayDx,rayDy,&dir);
wallHeight[i] = perspective(dist);
wallDir[i] = dir;
}
for (int i = 0; i < 10; ++i)
putchar('\n');
drawScreen();
char c = getchar();
switch (c) // movement
{
case 'a': camAngle -= 30; break;
case 'd': camAngle += 30; break;
case 'w': camX += forwX / 2; camY += forwY / 2; break;
case 's': camX -= forwX / 2; camY -= forwY / 2; break;
case 'q': quit = 1; break;
default: break;
}
}
return 0;
}
```
How to make this more advanced? Here are some hints and tips:
- **textured walls**: This is pretty simply, the ray hit basically gives us a horizontal texturing coordinate, and we simply stretch the texture vertically to fit the wall. I.e. when the ray hits a wall, we take the hit coordinate along the principal axis of the wall (e.g. for vertical hit we take the Y coordinate) and [mod](mod.md) it by the fixed point unit which will give us the texturing coordinate. This coordinate tells us the column of the texture that the rendered column shall have; we read this texture column and render it stretched vertically to fit the column height given by the perspective. Note that for [cache](cache.md) friendliness ([optimization](optimization.md)) textures should be stored column-wide in memory as during rendering we'll be reading the texture by columns (row-wise stored textures would make us jump wide distances in the memory which CPU caches don't like).
- **textured floor/ceiling**: Something aking [mode7](mode7.md) rendering can be used.
- **sliding door**: TODO
- **jumping**: Camera can easily be shifted up and down. If we are to place the camera e.g. one fixed point unit above its original position, then for each column we render we compute, with perspective applied to this one fixed point unit (the same way with which we determine the column size on the screen) the vertical screen-space offset of the wall and render this wall column that many pixel lower.
- **looking up/down**: Correct view of a camera that's slightly tilted up/down can't be achieved (at least not in a reasonably simple way), but there's a simple trick for faking it -- camera shearing. Shearing literally just shifts the rendered view vertically, i.e. if we're to look a bit up, we render that same way as usual but start higher up on the screen (in the part of the rendered image that's normally above the screen and not visible), so that the vertical center of the screen will be shifted downwards. For smaller angles this looks [good enough](good_enough.md).
- **multilevel floor/ceiling**: This is a bit more difficult but it can be done e.g. like this: for each level square we store its floor and ceiling height. When casting a ray, we will consider any change in ceiling and/or floor height a hit, AND we'll need to return multiple of those hits (not just the first one). When we cast a ray and get a set of such hits, from each hit we'll know there are tiny walls on the floor and/or ceiling, and we'll know their distances. This can be used to correctly render everything.
- **different level geometry**: In theory the level doesn't have to be a square grid but some kind of another representation, or we may keep it a square grid but allow placement of additional shapes in it such as cylinders etc. Here you simply have to figure out how to trace the rays so as to find the first thing it hits.
- **adding [billboards](billboard.md) (sprites)**: TODO
- **reflections**: We can make our 2D raycaster a 2D [raytracer](raytracing.md), i.e. when we cast a camera ray and it hits a reflective wall (a mirror), we cast another, secondary reflected ray and trace it to see which wall it hits, i.e. which wall will get reflected in the reflective wall.
- **partly transparent walls**: We can make some walls partially transparent, both with [alpha blending](alpha.md) or textures with transparent pixels. In both cases we'll have to look not just for the first hit of the ray, but also for the next.

View file

@ -30,7 +30,7 @@ Articles should be written to be somewhat readable and understandable to tech sa
These are some sources you can use for research and gathering information for articles:
- **[Wikipedia](wikipedia.md)**: of course, but don't limit your search to it.
- **[Wikipedia](wikipedia.md)**: of course, but don't limit your search to it. Searching other language Wikipedias with machine translate can also help find extra info.
- **[Citizendium](citizendium.md)**: can offer a different angle of view from Wikipedia.
- **Britannica online**: proprietary, but articles are nicely written, facts are in the public domain so we can steal them.
- **[wikiwikiweb](wikiwikiweb.md)**