Big Chimpin!
Avatar

Bian Jiang

Vcard Download vCard   what is this?
Rss_icon

Recent Activity


Filter by:
All
  • Google Go:初级读本

    from http://www.infoq.com/cn/articles/google-go-primer

    作者 Samuel Tesla 译者 黄璜 发布于 2010年4月2日 上午12时5分

    Google最近发布新型的编程语言,Go。它被设计为将现代编程语言的先进 性带入到目前仍由C语言占统治地位的系统层面。然而,这一语言仍在试验阶段并在不断演变。

    Go语言的设计者计划设计一门简单、高效、安全和 并发的语言。这门语言简单到甚至不需要有一个符号表来进行词法分析。它可以快速地编译;整个工程的编译时间在秒以下的情况是常事。它具备垃圾回收功能,因 此从内存的角度是安全的。它进行静态类型检查,并且不允许强制类型转换,因而对于类型而言是安全的。同时语言还内建了强大的并发实现机制。

    阅读Go

    Go的语法传承了与C一样的风格。程序由函数组成,而函数体是一系列的语句序列。一段代码块用花括号括起来。这门语言保留有限的关键字。表达式使用 同样的中缀运算符。语法上并无 太多出奇之处。

    Go语言的作者在设计这一语言时坚持一个单一的指导原则:简单明了至上。一些新的语法构件提供了简明地表达一些约定俗成的概 念的方式,相较之下用C表达显得冗长。而其他方面则是针对几十年的使用所呈现出来的一些不合理的语言选择作出了改进。

    变量声明

    变量是如下声明的:

    var sum int // 简单声明
    var total int = 42 // 声明并初始化

    最值得注意的是,这些声明里的类型跟在变量名的后面。乍一看有点怪,但这更清晰明了。比如,以下面这个C片段来说:

    int* a, b;

    它并明了,但这里实际的意思是a是一个指针,但b不是。如果要将两者都声明为指针,必须要重复星号。然后在Go语言里,通过如下方式可以将两者都 声明为指针:

    var a, b *int

    如果一个变量初始化了,编译器通常能推断它的类型,所以程序员不必显式的敲出来:

    var label = "name"

    然而,在这种情况下var几乎显得是多余了。因此,Go的作者引入了一个新的运算符来 声明和初始化一个新的变量:

    name := "Samuel"

    条件语句

    Go语言当中的条件句与C当中所熟知的if-else构造一样,但条件不需要被打包在括号内。这样可以减少阅读代码时的视觉上的混乱。

    括号并不是唯一被移去的视觉干扰。在条件之间可以包括一个简单的语句,所以如下的代码:

    result := someFunc();
    if result > 0 {
    /* Do something */
    } else {
    /* Handle error */
    }

    可以被精简成:

    if result := someFunc(); result > 0 { 
    /* Do something */
    } else {
    /* Handle error */
    }

    然而,在后面这个例子当中,result只在条件块内部有效——而前者 中,它在整个包含它的上下文中都是可存取的。

    分支语句

    分支语句同样是似曾相识,但也有增强。像条件语句一样,它允许一个简单的语句位于分支的表达式之前。然而,他们相对于在C语言中的分支而言走得更远。

    首先,为了让分支跳转更简明,作了两个修改。情况可以是逗号分隔的列表,而fall-throuth也不再是默认的行为。

    因此,如下的C代码:

    int result;
    switch (byte) {
    case 'a':
    case 'b':
    {
    result = 1
    break
    }

    default:
    result = 0
    }

    在Go里就变成了这样:

    var result int
    switch byte {
    case 'a', 'b':
    result = 1
    default:
    result = 0
    }

    第二点,Go的分支跳转可以匹配比整数和字符更多的内容,任何有效的表达式都可以作为跳转语句值。只要它与分支条件的类型是一样的。

    因此如下的C代码:

    int result = calculate();
    if (result < 0) {
    /* negative */
    } else if (result > 0) {
    /* positive */
    } else {
    /* zero */
    }

    在Go里可以这样表达:

    switch result := calculate(); true {
    case result < 0:
    /* negative */
    case result > 0:
    /* positive */
    default:
    /* zero */
    }

    这些都是公共的约定俗成,比如如果分支值省略了,就是默认为真,所以上面的代码可以这样写:

    switch result := calculate(); {
    case result < 0:
    /* negative */
    case result > 0:
    /* positive */
    default:
    /* zero */
    }

    循环

    Go只有一个关键字用于引入循环。但它提供了除do-while外C语言当中所有可用的循环方式。

    条件

    for a > b { /* ... */ }

    初始,条件和步进

    for i := 0; i < 10; i++ { /* ... */ }

    范围

    range语句右边的表达式必须是arrayslicestring或者map, 或是指向array的指针,也可以是channel

    for i := range "hello" { /* ... */ }

    无限循环

    for { /* ever */ }

    函数

    声明函数的语法与C不同。就像变量声明一样,类型是在它们所描述的术语之后声明的。在C语言中:

    int add(int a, b) { return a + b }

    在Go里面是这样描述的:

    func add(a, b int) int { return a + b }

    多返回值

    在C语言当中常见的做法是保留一个返回值来表示错误(比如,read()返回0),或 者保留返回值来通知状态,并将传递存储结果的内存地址的指针。这容易产生了不安全的编程实践,因此在像Go语言这样有良好管理的语言中是不可行的。

    认识到这一问题的影响已超出了函数结果与错误通讯的简单需求的范畴,Go的作者们在语言中内建了函数返回多个值的能力。

    作为例子,这个函数将返回整数除法的两个部分:

    func divide(a, b int) (int, int) {
    quotient := a / b
    remainder := a % b
    return quotient, remainder
    }

    有了多个返回值,有良好的代码文档会更好&mdash;&mdash;而Go允许你给返回值命名,就像参数一样。你可以对这些返回的变量赋值,就像其它的变量一样。所以我们可以重写divide:

    func divide(a, b int) (quotient, remainder int) {
    quotient = a / b
    remainder = a % b
    return
    }

    多返回值的出现促进了"comma-ok"的模式。有可能失败的函数可以返回第二个布尔结果来表示成功。作为替代,也可以返回一个错误对象,因此像下面这样的代码也就不见怪了:

    if result, ok := moreMagic(); ok {
    /* Do something with result */
    }

    匿名函数

    有了垃圾收集器意味着为许多不同的特性敞开了大门&mdash;&mdash;其中就包括匿名函数。Go为声明匿名函数提供了简单的语法。像许多动态语言一样,这些函数在它们被定义的范围内创建了词法闭包。

    考虑如下的程序:

    func makeAdder(x int) (func(int) int) {
    return func(y int) int { return x + y }
    }

    func main() {
    add5 := makeAdder(5)
    add36 := makeAdder(36)
    fmt.Println("The answer:", add5(add36(1))) //=> The answer: 42
    }

    基本类型

    像C语言一样,Go提供了一系列的基本类型,常见的布尔,整数和浮点数类型都具备。它有一个Unicode的字符串类型和数组类型。同时该语言还引入了两 种新的类型:slicemap

    数组和切片

    Go语言当中的数组不是像C语言那样动态的。它们的大小是类型的一部分,在编译时就决定了。数组的索引还是使用的熟悉的C语法(如 a[i]),并且与C一样,索引是由0开始的。编译器提供了内建的功能在编译时求得一个数组的长度 (如 len(a))。如果试图超过数组界限写入,会产生一个运行时错误。

    Go还提供了切片(slices),作为数组的变形。一个切片(slice)表示一个数组内的连续分段,支持程序员指定底层存储的明确部分。构建一个切片 的语法与访问一个数组元素类似:

    /* Construct a slice on ary that starts at s and is len elements long */
    s1 := ary[s:len]

    /* Omit the length to create a slice to the end of ary */
    s2 := ary[s:]

    /* Slices behave just like arrays */
    s[0] == ary[s] //=> true

    // Changing the value in a slice changes it in the array
    ary[s] = 1
    s[0] = 42
    ary[s] == 42 //=> true

    该切片所引用的数组分段可以通过将新的切片赋值给同一变量来更改:

    /* Move the start of the slice forward by one, but do not move the end */
    s2 = s2[1:]

    /* Slices can only move forward */
    s2 = s2[-1:] // this is a compile error

    切片的长度可以更改,只要不超出切片的容量。切片s的容量是数组从s[0]到数组尾端的大小,并由内建的cap()函数返回。一个切片的长度永远不能超出它的容量。

    这里有一个展示长度和容量交互的例子:

    a := [...]int{1,2,3,4,5} // The ... means "whatever length the initializer has"
    len(a) //=> 5

    /* Slice from the middle */
    s := a[2:4] //=> [3 4]
    len(s), cap(s) //=> 2, 3

    /* Grow the slice */
    s = s[0:3] //=> [3 4 5]
    len(s), cap(s) //=> 3, 3

    /* Cannot grow it past its capacity */
    s = s[0:4] // this is a compile error

    通常,一个切片就是一个程序所需要的全部了,在这种情况下,程序员根本用不着一个数组,Go有两种方式直接创建切片而不用引用底层存储:

    /* literal */
    s1 := []int{1,2,3,4,5}

    /* empty (all zero values) */
    s2 := make([]int, 10) // cap(s2) == len(s2) == 10

    Map类型

    几乎每个现在流行的动态语言都有的数据类型,但在C中不具备的,就是dictionary。Go提供了一个基本的dictionary类型叫做map。下 面的例子展示了如何创建和使用Go map:

    m := make(map[string] int) // A mapping of strings to ints

    /* Store some values */
    m["foo"] = 42
    m["bar"] = 30

    /* Read, and exit program with a runtime error if key is not present. */
    x := m["foo"]

    /* Read, with comma-ok check; ok will be false if key was not present. */
    x, ok := m["bar"]

    /* Check for presence of key, _ means "I don't care about this value." */
    _, ok := m["baz"] // ok == false

    /* Assign zero as a valid value */
    m["foo"] = 0;
    _, ok := m["foo"] // ok == true

    /* Delete a key */
    m["bar"] = 0, false
    _, ok := m["bar"] // ok == false

    面向对象

    Go语言支持类似于C语言中使用的面向对象风格。数据被组织成structs,然后定义操作这些structs的函数。类似于Python,Go语言提供 了定义函数并调用它们的方式,因此语法并不会笨拙。

    Struct类型

    定义一个新的struct类型很简单:

    type Point struct {
    x, y float64
    }

    现在这一类型的值可以通过内建的函数new来分配,这将返回一个指针,指向一块内存单元,其所占内存槽初始化为零。

    var p *Point = new(Point)
    p.x = 3
    p.y = 4

    这显得很冗长,而Go语言的一个目标是尽可能的简明扼要。所以提供了一个同时分配和初始化struct的语法:

    var p1 Point = Point{3,4}  // Value
    var p2 *Point = &Point{3,4} // Pointer

    方法

    一旦声明了类型,就可以将该类型显式的作为第一个参数来声明函数:

    func (self Point) Length() float {
    return math.Sqrt(self.x*self.x + self.y*self.y);
    }

    这些函数之后可作为struct的方法而被调用:

    p := Point{3,4}
    d := p.Length() //=> 5

    方法实际上既可以声明为值也可以声明为指针类型。Go将会适当的处理引用或解引用对象,所以既可以对类型T,也可以对类型*T声明方式,并合理地使用它们。

    让我们为Point扩展一个变换器:

    /* Note the receiver is *Point */
    func (self *Point) Scale(factor float64) {
    self.x = self.x * factor
    self.y = self.y * factor
    }

    然后我们可以像这样调用:

    p.Scale(2);
    d = p.Length() //=> 10

    很重要的一点是理解传递给MoveToXY的self和其它的参数一样,并且是传递,而不是引用传递。如果它被声明为Point,那么在方法内修改的struct就不再跟调用方的一样&mdash;&mdash;值在它们传递给方法的时候被 拷贝,并在调用结束后被丢弃。

    接口

    像Ruby这样的动态语言所强调面向对象编程的风格认为对象的行为比哪种对象是动态类型(duck typing)更为重要。Go所 带来的一个最强大的特性之一就是提供了可以在编程时运用动态类型的思想而把行为定义的合法性检查的工作推到编译时。这一行为的名字被称作接口

    定义一个接口很简单:

    type Writer interface {
    Write(p []byte) (n int, err os.Error)
    }

    这里定义了一个接口和一个写字节缓冲的方法。任何实现了这一方法的对象也实现了这一接口。不需要像Java一样进行声明,编译器能推断出来。这既给予了动态类型的表达能力又保留了静态类型检查的安全。

    Go当中接口的运作方式支持开发者在编写程序的时候发现程序的类型。如果几个对象间存在公共行为,而开发者想要抽象这种行为,那么它就可以创建一个接口并使用它。

    考虑如下的代码:

    // Somewhere in some code:
    type Widget struct {}
    func (Widget) Frob() { /* do something */ }

    // Somewhere else in the code:
    type Sprocket struct {}
    func (Sprocket) Frob() { /* do something else */ }

    /* New code, and we want to take both Widgets and Sprockets and Frob them */
    type Frobber interface {
    Frob()
    }

    func frobtastic(f Frobber) { f.Frob() }

    需要特别指出的很重要的一点就是所有的对象都实现了这个空接口:

    interface {}

    继承

    Go语言不支持继承,至少与大多数语言的继承不一样。并不存在类型的层次结构。相较于继承,Go鼓励使用组合和委派,并为此提供了相应的语法甜点使其更容易接受。

    有了这样的定义:

    type Engine interface {
    Start()
    Stop()
    }

    type Car struct {
    Engine
    }

    于是我可以像下面这样编写:

    func GoToWorkIn(c Car) {
    /* get in car */

    c.Start();

    /* drive to work */

    c.Stop();

    /* get out of car */
    }

    当我声明Car这个struct的时候,我定义了一个匿名成员。这是一 个只能被其类型识别的成员。匿名成员与其它的成员一样,并有着和类型一样的名字。因此我还可以写成c.Engine.Start()。 如果Car并没有其自身方法可以满足调用的话,编译器自动的会将在Car上的调用委派给它的Engine上面的方法。

    由匿名成员提供的分离方法的规则是保守的。如果为一个类型定义了一个方法,就使用它。如果不是,就使用为匿名成员定义的方法。如果有两个匿名成员都提供一 个方法,编译器将会报错,但只在该方法被调用的情况下。

    这种组合是通过委派来实现的,而不是继承。一旦匿名成员的方法被调用,控制流整个都被委派给了该方法。所以你无法做到和下面的例子一样来模拟类型层次:

    type Base struct {}
    func (Base) Magic() { fmt.Print("base magic") }
    func (self Base) MoreMagic() {
    self.Magic()
    self.Magic()
    }

    type Foo struct {
    Base
    }
    func (Foo) Magic() { fmt.Print("foo magic") }

    当你创建一个Foo对象时,它将会影响Base的两个方法。然而,当你调用MoreMagic时, 你将得不到期望的结果:

    f := new(Foo)
    f.Magic() //=> foo magic
    f.MoreMagic() //=> base magic base magic

    并发

    Go的作者选择了消息传递模型来作为推荐的并发编程方法。该语言同样支持共享内存,然后作者自有道理:

    不要通过共享内存来通信,相反,通过通信来共享内存。

    该语言提供了两个基本的构件来支持这一范型:goroutineschannels

    Go例程

    Goroutine是轻量级的并行程序执行路径,与线程,coroutine或者进程类似。然而,它们彼此相当不同,因此Go作者决定给它一个新的名字并 放弃其它术语可能隐含的意义。

    创建一个goroutine来运行名为DoThis的函数十分简单:

    go DoThis() // but do not wait for it to complete

    匿名的函数可以这样使用:

    go func() {
    for { /* do something forever */ }
    }() // Note that the function must be invoked

    这些goroutine将会通过Go运行时而映射到适当的操作系统原语(比如,POSIX线程)。

    通道类型

    有了goroutine,代码的并行执行就容易了。然而,它们之间仍然需要通讯机制。Channel提供一个FIFO通信队列刚好能达到这一目的。

    以下是使用channel的语法:

    /* Creating a channel uses make(), not new - it was also used for map creation */
    ch := make(chan int)

    /* Sending a value blocks until the value is read */
    ch <- 4

    /* Reading a value blocks until a value is available */
    i := <-ch

    举例来说,如果我们想要进行长时间运行的数值计算,我们可以这样做:

    ch := make(chan int)

    go func() {
    result := 0
    for i := 0; i < 100000000; i++ {
    result = result + i
    }
    ch <- result
    }()

    /* Do something for a while */

    sum := <-ch // This will block if the calculation is not done yet
    fmt.Println("The sum is:", sum)

    channel的阻塞行为并非永远是最佳的。该语言提供了两种对其进行定制的方式:

    1. 程序员可以指定缓冲大小&mdash;&mdash;想缓冲的channel发送消息不会阻塞,除非缓冲已满,同样从缓冲的channel读取也不会阻塞,除非缓冲是空的。
    2. 该语言同时还提供了不会被阻塞的发送和接收的能力,而操作成功是仍然要报告。
    /* Create a channel with buffer size 5 */
    ch := make(chan int, 5)

    /* Send without blocking, ok will be true if value was buffered */
    ok := ch <- 42

    /* Read without blocking, ok will be true if a value was read */
    val, ok := <-ch

    Go提供了一种简单的机制来组织代码:包。每个文件开头都会声明它属于哪一个包,每个文件也可以引入它所用到的包。任何首字母大写的名字是由包导出的,并可以被其它的包所使用。

    以下是一个完整的源文件:

    package geometry

    import "math"

    /* Point is capitalized, so it is visible outside the package. */

    type Point struct {

    /* the fields are not capitalized, so they are not visible
    outside of the package */

    x, y float64
    }

    /* These functions are visible outside of the package */

    func (self Point) Length() float64 {
    /* This uses a function in the math package */
    return math.Sqrt(self.x*self.x + self.y*self.y)
    }

    func (self *Point) Scale(factor float64) {
    self.setX(self.x * factor)
    self.setY(self.y * factor)
    }

    /* These functions are not visible outside of the package, but can be
    used inside the package */

    func (self *Point) setX(x float64) { self.x = x }
    func (self *Point) setY(y float64) { self.y = y }

    缺失

    Go语言的作者试图将代码的清晰明确作为设计该语言作出所有决定的指导思想。第二个目标是生产一个编译速度很快的语言。有了这两个标准作为方向,来 自其它语言的许多特性就不那么适合了。许多程序员会发现他们最爱的语言特性在Go当中不存在,确实,有很多人也许会觉得Go语言由于缺乏其它语言所共有的 一些特性,还不太可用。

    这当中两个缺失的特性就是异常和泛型,两者在其它语言当中都是非常有用的。而它们目前都不是Go的一分子。但因为该 语言仍处于试验阶段,它们有可能最终会加入到语言里。然而,如果将Go与其它语言作比较的话,我们应当记住Go是打算在系统编程层面作为C语言的替代。明 白这一点的话,那么缺失的这许多特性倒也不是很大的问题了。

    最后,因为这一语言才刚刚发布,因此它没有什么类库或工具可以用,也没有Go语 言的集成编程环境。Go语言标准库有些有用的代码,但这与更为成熟的语言比 起来仍还是很少的。

    查看英文原文Google Go: A Primer


    感谢马国耀对本文的审校。

    给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

  • 用JavaScript玩转计算机图形学(二)基本光源

    阅读: 989 评论: 11 作者: Milo Yip 发表于 2010-04-02 20:44 原文链接

    上一篇介绍了简单的光线追踪,凑合了临时用的光源去渲染效果。这次将讲解三种基本光源,及一些背景理论。过分简化的教材和现成API(OpenGL/Direct3D等)可能会做成一些错误理解。在此,希望文章能简单之余,又不失背后理论。读者明白之后,可把概念简化,或按实际情况调整。

    本文代码可在此下载(10KiB)。

    读者若喜欢本文,可按推荐按钮以示鼓励。如果写得不够清楚,或有错误之处,可留言相告。

    在物理上,光(light)可以视为电磁波(electromagnetic wave)或光子(photon)。在计算机图形学的领域里,通常只会用到光的部份物理性质,例如假设光是直线前进(不受因引力影响),忽略光的速度,通常不考虑衍射(diffraction)、干涉(interference )等等(好吧,也不考虑量子行为☺)。因为,计算机图形学不是理物学,最终目标(笔者认为)只是要渲染视觉上美的事物,只要模拟到某个合适层次的模型,有时候还为了美观而采用非物理/非真实的方式。

    方向光源

    光源(light source)放射(emit)光,而非散射(scatter)或吸收(absorb)光。

    最简单的光源模型,是方向光源(directional light),又称平行光源。这种光源假设光在无限远放射,在任何位置,放射方向都是一致的,可以模拟类似太阳的光线(虽然实际上太阳并非无限远)。

    方向光源的方向,通常用光向量(light vector)去表示。为方便计算,通常是单位向量,并且和光的放射方向相反

    方向光源的另一个属性,是指定其照明的量。量度光的科学叫幅射度量学(radiometry),本文暂且略过其细节。这里只用到光的其中一个量度方式,就是每秒通过每单位面积平面的光子总能量,称为幅照度(irradiance)。

    光的颜色,是由不同频率的光波及其频谱,在人类视觉上形成的。详细内容又涉及光度测定(photometry)、比色法(colorimetry)、视觉感知(visual perception)、甚至哲学等,有机会再谈。这里只使用常见的红绿蓝三个颜色通道(color channel)。光源的幅照度也可以用这三通道来描述,因此,仍可用前文的Color类来描述幅照度。但注意,光的幅照度范围是零到无限大,并不是[0,1]或[0,255]。光的"颜色"和材质的"颜色"并非同一个概念,关于这点,读者可思考以下一个简单命题

    客观上,有接近白色的纸,但没有白色的光

    关于这个命题,和材质的"颜色",将于下回分解。

    阴影

    一个光源的阴影(shadow),是因不透明障碍物,以致其不能到达的地方。我们可使用已有的几何相交功能,去检测某一位置,在方向上有否障碍物。光源追踪方法在阴影处理上很简单,光删化方法就复杂得多。

    实现DirectionalLight类

    在编程时,需要为不同种类的光源设计一个共通接口。渲染器要从光源取得,在某个空间位置,其光向量和幅照度。在此,定义光源有一成员函数sample(scene, position),并传回一个LightSample对象:

    LightSample = function(L, EL) { this.L = L; this.EL = EL; };
    LightSample.zero = new LightSample(Vector3.zero, Color.black);
    

    以下是方向光源的代码,预设使用阴影:

    DirectionalLight = function(irradiance, direction) { this.irradiance = irradiance; this.direction = direction; this.shadow = true; };
    
    DirectionalLight.prototype = {
        initialize: function() { this.L = this.direction.normalize().negate(); },
    
        sample: function(scene, position) {
            // 阴影测试
            if (this.shadow) {
                var shadowRay = new Ray3(position, this.L);
                var shadowResult = scene.intersect(shadowRay);
                if (shadowResult.geometry)
                    return LightSample.zero;
            }
    
            return new LightSample(this.L, this.irradiance);
        }
    };
    

    渲染幅照度

    sample()函数可以传回相对光向量的幅照度,但物体表面并不一定垂直于光向量。光源越接近平面,每面积接受的能量就越少。可以想像太阳在中午是最亮的,日出日落时是最暗的。如下图所示,平面法向量方向的面积,是光向量方向的面积的倍,而幅照度则为其倒数,即倍。

    因此,设光源的光向量方向幅照度为,平面接收到的幅照度为

     

    幅照度是能量,可以累加,所以多个光源下,平面接收到的总幅照度为

    以下的简单代码,测试一个方向光源在场境中的总幅照度:

    function renderLight(canvas, scene, lights, camera) {
        // 从canvas取得imgdata和pixels,跟之前的代码一样
        // ...
    
        scene.initialize();
        for (var k in lights)
            lights[k].initialize();
        camera.initialize();
    
        var i = 0;
        for (var y = 0; y < h; y++) {
            var sy = 1 - y / h;
            for (var x = 0; x < w; x++) {
                var sx = x / w;
                var ray = camera.generateRay(sx, sy);
                var result = scene.intersect(ray);
                if (result.geometry) {
                    var color = Color.black;
                    for (var k in lights) {
                        var lightSample = lights[k].sample(scene, result.position);
    
                        if (lightSample != lightSample.zero) {
                            var NdotL = result.normal.dot(lightSample.L);
    
                            // 夹角小约90度,即光源在平面的前面
                            if (NdotL >= 0)
                                color = color.add(lightSample.EL.multiply(NdotL));
                        }
                    }
                    pixels[i] = color.r * 255;
                    pixels[i + 1] = color.g * 255;
                    pixels[i + 2] = color.b * 255;
                    pixels[i + 3] = 255;
                }
                i += 4;
            }
        }
    
        ctx.putImageData(imgdata, 0, 0);
    }
    
    

    renderLight( document.getElementById('renderCanvas1'), new Union([ new Plane(new Vector3(0, 1, 0), 0), new Plane(new Vector3(0, 0, 1), -50), new Plane(new Vector3(1, 0, 0), -20), new Sphere(new Vector3(0, 10, -10), 10) ]), [new DirectionalLight(Color.white, new Vector3(-1.75, -2, -1.5))], new PerspectiveCamera(new Vector3(0, 10, 10), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90));

    Run

     

    修改代码试试看

    • 改變光源的顏色 (也試試超過1的值)
      改變光源的方向 (在DirectionalLight.initialize()裡自動做了normalize,這輸入不需位單位向量)
      改變光源的幅照度 (也試試超過1的值)
    • 改變光源的方向 (在DirectionalLight.initialize()裡自動做了normalize,這輸入不需位單位向量)

    点光源

    点光源/点光灯(point light),又称全向光源/泛光源/泛光灯(omnidirectional light/omni light),是指一个无限小的点,向所有光向平均地散射光。

    其光向量,就是表面位置往点光源位置的方向:

    学习物理时,经常有这种往所有方向发射的情况(例如引力、声音等)。类比可知,接收到的能量和距离的关系,是成平方反比定律的:

      

     当中I为幅射强度(intensity, radiant intensity),当r=1时,幅射强度和幅照度相等。

    通常称为衰减(attenuation)系数。有时候会为各种需求,写一些非物理正确的衰减系数。

    实现PointLight类

    以下代码中,不直接使用normalize(),令r和其平方可以在之后分别使用,算是简单的优化。

    PointLight = function(intensity, position) { this.intensity = intensity; this.position = position; this.shadow = true; };
    
    PointLight.prototype = {
        initialize: function() { },
        sample: function(scene, position) {
            // 计算L,但保留r和r^2,供之后使用
            var delta = this.position.subtract(position);
            var rr = delta.sqrLength();
            var r = Math.sqrt(rr);
            var L = delta.divide(r);
    
            // 阴影测试
            if (this.shadow) {
                var shadowRay = new Ray3(position, L);
                var shadowResult = scene.intersect(shadowRay);
                // 在r以内的相交点才会遮蔽光源
                if (shadowResult.geometry && shadowResult.distance <= r)
                    return LightSample.zero;
            }
    
            // 平方反比衰减
            var attenuation = 1 / rr;
    
            // 计算幅照度
            return new LightSample(L, this.intensity.multiply(attenuation));
        }
    };
    

    renderLight( document.getElementById('renderCanvas2'), new Union([ new Plane(new Vector3(0, 1, 0), 0), new Plane(new Vector3(0, 0, 1), -50), new Plane(new Vector3(1, 0, 0), -20), new Sphere(new Vector3(0, 10, -10), 10) ]), [new PointLight(Color.white.multiply(2000), new Vector3(30, 40, 20))], new PerspectiveCamera(new Vector3(0, 10, 10), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90));

    Run

     

    修改代码试试看

    • 改变幅射强度
    • 移动光源
    • 加入多一个点光源

    聚光灯

    现实中,并不存在理想的点光源,放射的光在不同方向是有差异的。聚光灯(spot light)是常用的一种模式,它在点光源的基础上,加入圆锥形的范围。聚光灯可以有不同的模型,以下采用Direct3D固定功能管道(fixed-function pipeline)用的模型做示范。

    聚光灯有一个主要方向s,再设置两个圆锥范围,称为内圆锥和外圆锥,两圆锥之间的范围称为半影(penumbra)。内外圆锥的内角分别为。聚光灯可计算一个聚光灯系数,范围为[0,1],代表某方向的放射比率。内圆锥中系数为1(最亮),内圆锥和外圆锥之间系数由1逐渐变成0。另外,可用另一参数p代表衰减(falloff),决定内圆锥和外圆锥之间系数变化。方程式如下:

    实现SpotLight类

    SpotLight类只是多了那几个参数,以计算聚光灯系数,最后结合到幅照度。很多参数可在initialize()里预计算,减少在sample()里重复运算。

    SpotLight = function(intensity, position, direction, theta, phi, falloff) {
        this.intensity = intensity;
        this.position = position;
        this.direction = direction;
        this.theta = theta;
        this.phi = phi;
        this.falloff = falloff;
        this.shadow = true;
    };
    
    SpotLight.prototype = {
        initialize: function() {
            this.S = this.direction.normalize().negate();
            this.cosTheta = Math.cos(this.theta * Math.PI / 180 / 2);
            this.cosPhi = Math.cos(this.phi * Math.PI / 180 / 2);
            this.baseMultiplier = 1 / (this.cosTheta - this.cosPhi);
        },
    
        sample: function(scene, position) {
            // 计算L,但保留r和r^2,供之后使用
            var delta = this.position.subtract(position);
            var rr = delta.sqrLength();
            var r = Math.sqrt(rr);
            var L = delta.divide(r);
    
            // 计算聚光灯因子
            var spot;
            var SdotL = this.S.dot(L);
            if (SdotL >= this.cosTheta)
                spot = 1;
            else if (SdotL <= this.cosPhi)
                spot = 0;
            else
                spot = Math.pow((SdotL - this.cosPhi) * this.baseMultiplier, this.falloff);
    
            // 阴影测试
            if (this.shadow) {
                var shadowRay = new Ray3(position, L);
                var shadowResult = scene.intersect(shadowRay);
                // 在r以内的相交点才会遮蔽光源
                if (shadowResult.geometry && shadowResult.distance <= r)
                    return LightSample.zero;
            }
    
            // 平方反比衰减
            var attenuation = 1 / rr;
    
            // 计算幅照度
            return new LightSample(L, this.intensity.multiply(attenuation * spot));
        }
    };
    

    renderLight( document.getElementById('renderCanvas3'), new Union([ new Plane(new Vector3(0, 1, 0), 0), new Plane(new Vector3(0, 0, 1), -50), new Plane(new Vector3(1, 0, 0), -20), new Sphere(new Vector3(0, 10, -10), 10) ]), [new SpotLight(Color.white.multiply(2000), new Vector3(30, 40, 20), new Vector3(-1, -1, -1), 20, 30, 0.5)], new PerspectiveCamera(new Vector3(0, 10, 10), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90));

    Run

     

    修改代码试试看

    • 改变各个参数

    例子

    三原色

    这个例子把三原色聚光灯重叠射度地板,可以看到它们的颜色混合。

    renderLight( document.getElementById('renderCanvas4'), new Union([ new Plane(new Vector3(0, 1, 0), 0), new Plane(new Vector3(0, 0, 1), -50), new Plane(new Vector3(1, 0, 0), -20) ]), [ new PointLight(Color.white.multiply(1000), new Vector3(30, 40, 20)), new SpotLight(Color.red.multiply(3000), new Vector3(0, 30, 10), new Vector3(0, -1, -1), 20, 30, 1), new SpotLight(Color.green.multiply(3000), new Vector3(6, 30, 20), new Vector3(0, -1, -1), 20, 30, 1), new SpotLight(Color.blue.multiply(3000), new Vector3(-6, 30, 20), new Vector3(0, -1, -1), 20, 30, 1) ], new PerspectiveCamera(new Vector3(0, 40, 15), new Vector3(0, -1.25, -1), new Vector3(0, 1, 0), 60));

    Run

     

    修改代码试试看

    • 如果,幅射强度是负值的话,会怎么样?(虽然未证实反光子(antiphoton)的存在,但读者能想到图形学上的功能么?)

    很多光源

    这个例子在天花加了36个点光源,和一个从后往前的填充用方向光源。有时候灯光师会加入填充光源(fill light),去加强对象的轮廓及立体感(有时候用上冷暖色的对比)。这个渲染比较慢,可能要半分钟啊!

    var lights = []; for (var x = 10; x <= 30; x += 4) for (var z = 20; z <= 40; z += 4) lights.push(new PointLight(Color.white.multiply(80), new Vector3(x, 50, z))); // var fillLight = new DirectionalLight(Color.white.multiply(0.25), new Vector3(1.5, 1, 0.5)); fillLight.shadow = false; lights.push(fillLight); // renderLight( document.getElementById('renderCanvas5'), new Union([ new Plane(new Vector3(0, 1, 0), 0), new Plane(new Vector3(0, 0, 1), -50), new Plane(new Vector3(1, 0, 0), -20), new Sphere(new Vector3(0, 10, -10), 10) ]), lights, new PerspectiveCamera(new Vector3(0, 10, 10), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90));

    Run

     

    修改代码试试看

    • 把光源放在不同位置(例如接近地面)
    • 把每个光源的颜色加入差异

    结语

    本文简单介绍了三种基本的光源,这些光源除了应用在光线追踪渲染器上,也常用在光栅化渲染器中。

    除这三种以外,还有一类比较高阶的光源──面光源(area light)。面光源比这三种光源更真实,也能完美地做到真实的柔和阴影。如果能实现面光源,基本上也不用特定做「光源」这种类,取而代之,可以设定某些材质本身能发光即可。当然,没有免费午餐,随之而来的时间复杂度也增加。

    有了光源,下一篇大概会开始谈材质,讲述光源和材质间的互动。

    参考

    • Tomas Möller, Eric Haines, Naty Hoffman, Real-time Rendering 3rd Edition, AK Peters 2008
    • Matt Pharr, Greg Humphreys, Physically Based Rendering, Morgan Kaufmann, 2004

    评论: 11 查看评论 发表评论

    找优秀程序员,就在博客园


    最新新闻:
    · 雅虎针对iPad推出雅虎娱乐应用(2010-04-02 23:03)
    · 纳斯达克针对iPad推出投资组合管理应用(2010-04-02 23:01)
    · 《时代》杂志iPad应用售价4.99美元(2010-04-02 22:59)
    · 夏普今年将推3D 面板 可裸眼观看3D图片(2010-04-02 22:56)
    · 现有软件编写方式阻碍发挥多核潜力?(2010-04-02 22:14)

    编辑推荐:时代周刊:iPad能否让乔布斯续写传奇

    网站导航:博客园首页  个人主页  新闻  闪存  小组  博问  社区  知识库

  • 各省定居国外官员人数一览表

    源引自人民日报资料库

    自一九九二年以来,外逃省部级(包括副省部级)

    地  区    人 数  地厅级或以上官员  携带资金(美元)

    北京市     225人     58人      25亿

    天津市     122人     19人      14亿

    河北省     340人     26人      31亿

    山西省     236人     41人      17亿

    辽宁省     367人     52人      117亿

    吉林省     117人     14人      26亿

    黑龙江     230人     42人      85亿

    上海市     206人     66人      250亿

    江苏省     313人     40人      140亿

    浙江省     142人     48人      86亿

    安徽省     97人     19人      30亿

    福建省     480人     102人      365亿

    江西省     125人     21人      26亿

    山东省     352人     54人      150亿

    河南省     124人     27人      50亿

    湖北省     365人     33人      60亿

    湖南省     300人     20人      70亿

    广东省    1640人     170人     1550亿

    广 西     217人     25人      55亿

    海南省     140人     22人      27亿

    重庆市     175人     31人      18亿

    四川省     144人     40人      50亿

    云南省     238人     48人      60亿

    陕西省     146人     16人      28亿

    新 疆     260人     24人      30亿

    另附: 行政管理费(或曰公务支出)在国家财政支出中的比重:

    德 国(1998年) 2.7%

    埃 及(1997年) 3.1%

    英 国(1999年) 4.2%

    韩 国(1997年) 5.1%

    泰 国(2000年) 5.2%

    印 度(2000年) 6.3%

    加拿大(2000年) 7.1%

    俄罗斯(2000年) 7.6%

    美 国(2000年) 9.9%

    中 国(2000年)25.7%

    用于教育,医疗的比列:

    中国:3.8%

    印度:19.7%

    美国:21.5%

    日本:23.3%

    相关日志

    • 无相关日志
  • [公务员] 公务员买房内部价惊人!zz 发信人: YYJN (烟雨江南), 信区: Career_Servant
    标 题: 公务员买房内部价惊人!zz
    发信站: 水木社区 (Tue Mar 30 20:33:50 2010), 站内

    我是来问问是不是真的的?
    http://bbs.gz.house.163.com/bbs/junjing/171275586.html

    公务员买房内部价惊人!
    最近发现北京的公务员集资购房和团购房真是便宜的吓人!怪不得人人都争当公务员啊!

    市公务员集资房,位置刘家窑桥西南,均价4000/平,周边30000/平
    铁道部集资房,位置西直门铁科院院内,均价2000/平,周边50000/平
    公安部集资房,位置广渠门外,均价4500/平,周边35000/平
    外交部团购房,位置双井桥东南,项目名称:禧福会国际社区,团购价6000/平,市场价30000/平
    外交部集资建房,位置劲松桥 项目名称:和谐雅园 集资价:5800/平 市场价30000/平
    中石油团购房,位置太阳宫地铁站,项目名称:太阳星城,团购价8800/平,市场价28000/平
    市区政府团购房,位置大红门,项目名称:京投快线·阳光花园,团购价6000/平,市场价25000/平
    市发改委建委集资房,位置六里桥西局,均价4000/平,市场价30000/平
    人民银行集资房,位置宣武门康乐里,均价2000/平,市场价50000/平
    市铁路局集资房,位置广安门手帕口南,均价5000/平,市场价32000/平
    中信银行团购房,位置菜市口,项目:中信城,团购价5000/平,市场价33000/平

    朋友们!这实在太让人无语了!大家一起来818吧!

    --

    ※ 来源:·水木社区 newsmth.net·[FROM: 125.39.143.*]
  • 中国十大令人寒心的冷笑话
    来源:新浪论坛

    1、秦始皇修筑万里长城时死了许多人,孟姜女的丈夫万喜良也在其中。听到这个消息,孟姜女只觉得天昏地暗,一下子昏倒在地,醒来后,她伤心地痛哭起来,只哭得天愁地惨,日月无光。不知哭了多久,忽听得天摇地动般地一声巨响,长城崩塌了几十里,露出了数不清的尸骨。孟姜女咬破手指,把血滴在一具具的尸骨上,她心里暗暗祷告:如果是丈夫的尸骨,血就会渗进骨头,如果不是,血就会流向四方。终于,孟姜女用这种方法找到了万喜良的尸骨。她抱着这堆白骨,哭着说道: “老万,你的死跟你丫本人素质不高有关啊!”

    ----1130日,七煤公司一领导在接受采访时表示,“11·27”矿难的主要原因归咎于井下矿工对规章制度执行不力,劳动者的素质离我们的要求还差很远。


    2、武松醉打蒋门神、替施恩夺了快活林之后,中了张都监、张团练的计,几乎命丧飞云浦。武松杀了张都监的几名爪牙,寻思了半晌,怨恨冲天:“不杀得张都监,如何出得这口恨气!”便去死尸身边解下腰刀,选好的取把将来跨了,拣条好朴刀提着,直奔孟州城张都监的后花园。张都监、张团练、蒋门神正在鸳鸯楼吃酒,冷不防武松闯了进来,噗噗几刀砍死蒋门神、张团练。武松踏着张都监的脑袋喝道:“你们这帮贼子,为何黑道白道勾结、串通一气害我?”张都监颤颤巍巍地答道:“说句实话,官匪勾结的重要原因,是我们的待遇过低了!”

    ----成都火车站派出所副所长付小华接受采访时表示:“出现‘警匪勾结’这种情况的重要原因是 pol.ice待遇过低”。

    3、董存瑞牺牲后到了天堂,上帝问他:“你是怎么死的?”董存瑞说:“为了炸敌人的碉堡,被de- tona-tor包炸死的”;上帝听后勃然大怒,说道:“胡说!你胆敢骗我?”董存瑞说:“我没骗您啊!”上帝说:“你以为我不懂科学吗?谁不知道,爆炸只会产生水和二氧化碳,你不是被水淹死的、就是被二氧化碳薰死的,怎么可能是被炸死的呢?!”

    ----吉林石化的人所说:爆炸产生水和二氧化碳,不会污染水源

    4、孔子路过泰山脚下,有一个妇女在墓前哀伤地哭泣。孔子手扶车沿听她哭诉,并让弟子 问她缘由,妇女说:“以前我的公公被老虎咬死,我的丈夫跟着被老虎咬死,现在我的儿子也被老虎咬死了;”孔子说:“事情都过去了,又何必伤心?那为什么不离开这里呢?”妇女说:“我怕失去低收入者作为纳税人的荣誉!”孔子于是对弟子道:“小子识之,苛政虽猛于虎,然纳税人的荣誉牛b于苛政也!”

    ----全国人大农业与农村委员会委员任正隆则认为,起征点太高剥夺了低收入者作为“纳税人”的荣誉。

    5、汉朝的淮南王刘安派人进山访仙,从仙翁手里得到了一张仙方。他把自己关进暗房里,炼起仙丹来。八卦炉里炼出一些圆滚滚的仙丹,他一口气吞下5颗,飘飘悠悠飞上天去了!门外的鸡犬一看,也跟着大吃起来,不一会,空中一阵鸡鸣狗叫,原来它们也飞上天了!有人问道:“刘安,你家的鸡犬怎么也跟着成仙了?”刘安说: “为了防止拉登发动kb袭击、撞击天庭,我特意实行“一人得道,鸡犬升天”制,在任何紧急情况下,都能及时帮助疏散与救援,这是一个安全上的举措,并不是专门把成仙作为福利”。

    ----广州地铁线网听政会上,地铁员工家属免费坐地铁引起代表争议,地铁总经理解释,是为了“反恐需要”。

    6、三国演义里,诸葛亮造木牛流马,用来运送粮草,以此大败曹军。但后来木牛流马却失传了,即便是诸葛亮的得意弟姜维也不会造。诸葛军师临终前众将问他:“军师,木牛流马这般好用,为何您再也不造了?” 孔明长叹一声曰:“某交通学大学士、大教授的研究结果表明,木牛流马的污染比汽车飞机大,为了子孙后代的幸福,你还是等着坐汽车吧!”

    ----“中国城市环境污染不是由汽车造成的,而是由自行车造成的”。国内一家搞环境研究的权威机构经过一番调查与研后得出的一个“科学”结论。

    7、老栓也向那边看,却只见一堆人的后背;颈项都伸得很长,仿佛许多鸭,被无形的手捏住了的,向上提着。静了一会,似乎有点声音,便又动摇起来,轰的一声,都向后退;一直散到老栓立着的地方,几乎将他挤倒了。 “喂!一手交钱,一手交货!”一个浑身黑色的人,站在老栓面前,眼光正像两把刀,刺得老栓缩小了一半。那人一只大手,向他摊着;一只手却撮着一个鲜红的馒头,那红的还是一点一点的往下滴。 老栓慌忙摸出洋钱,抖抖的想交给他,却又不敢去接他的东西。那人便焦急起来,嚷道,“怎么?嫌贵?舍不得银子?” 老栓还踌躇着,黑的人便抢过灯笼,一把扯下纸罩,裹了馒头,塞与老栓;一手抓过洋钱,捏一捏,转身去了。嘴里哼着说:“这血馒头是药,不能当馒头卖!价格不贵,不同意降价!”

    ----“药品怎么能当馒头卖?”在“看病难,药价贵”呼声高涨时,东盛制药集团总裁陶朝辉却反其道而行之,抛出“馒头论”,坚持“药价不贵,不同意降价”。

    8、宋代穷儒陈世美,进京考中状元,被招为驸马。其发妻秦香莲带二子上京寻亲,陈世美翻脸不认人;秦香莲悲痛欲绝,发誓要讨还情债。陈世美勃然大怒,上表朝廷奏曰:臣以为,开封自古就是神圣之地,岂容外地人随便进入?应该建立人口准入制度!同时,对那些恶意讨情之人,应坚决打击!”

    ----在刚刚结束的北京市“两会”上,政协委员张惟英教授提出“建立人口准入制度”的建议:目前北京市的居住人口已超过各种资源的人口承载极限,严重制约了北京的发展,建议摸清北京市实际需要的人才类别,用准入制度进行合理的引入,规范人口流动。

    9、有一日,窦娥碰到苏三、杨乃武、小白菜等人,就问他们:“你们都平反昭雪了吗?”众人说:“都昭雪了”;窦娥又问:“那少奇兄弟、德怀兄弟、志新妹妹呢?”众人说:“也都平反了”。窦娥便道:“我说什么来着,咱们的司法就是公正!那么多案件从错的纠成正的,这难道不是司法公正的体现吗?”

    ----被无辜关押11年的佘祥林被宣告无罪了,但这一悲剧投石入湖的震荡,远远没有平息。当事人申请国家赔偿、责任人被追究法律责任,尚都在公众的持续关注中。种种怨怒未消之下,另一方面却居然频频出现奇怪的言论:41日湖北高院向该省法院系统发出通知,要求认真总结避免佘祥林被冤杀的经验;最高法副院长万鄂湘日前在就此案答媒体问时又说:“是否司法不公应该从最后纠正的结果看。这个案件从错的又纠成正的,难道不是司法公正的体现吗?”

    10、一天,周扒皮去找刘文彩,“刘大哥,我们村那些穷棒子们发牢骚,说他们活得太苦、活得没意思”; 刘文彩说:“他们是我国巨大的财富,没有他们的辛苦哪有咱们少数人的享乐,他们的存在和维持现在的状态是很有必要的。” 周扒皮说:“有的长工说他想读书!” 刘文彩道:“咱们的教育改革已经成功了,他还嚷嚷个屁!” 周扒皮说:“他们说收租院放高利贷是暴利”; 刘文彩道:“放高利贷就该暴利,谁让他们不幸生在x国了?我们就是要把暴利进行到底!” 周扒皮说:“他们还说现在收入差距过大,存在两极分化”; 刘文彩道:“纯属放屁!大家都在同一个经纬度上,又不是一个在南极、一个在北极,哪来的两极分化?!”

    ----经济学家厉以宁如是说“8亿多农民和下岗工人是中国巨大的财富,没有他们的辛苦哪有少数人的享乐,他们的存在和维持现在的状态是很有必要的。”
    GFW Blog入围德国之声国际博客大赛记者无疆界特别奖,敬请大家帮忙投票。翻墙利器"赛风"(Psiphon)代理新网址:http://no21984.org/。被墙网站收集:http://delicious.com/GFWbookmark,请使用GFWlist为标签,帮助我们收集被墙网站的信息。敬请订阅GFW Blog:http://feeds2.feedburner.com/chinagfwblog,邮件订阅:https://groups.google.com/group/gfw-blog。更多翻墙工具介绍和下载: 推客浏览器(http://twitbrowser.net/blog/,墙内镜像:http://tm005.nl.am/),Sesawe(http://www.sesawwe.net/)。翻墙互助小组邮件列表: http://groups.google.com/group/bypassgfw。
  • 用JavaScript玩转计算机图形学(一)光线追踪入门

    阅读: 8465 评论: 55 作者: Milo Yip 发表于 2010-03-29 00:05 原文链接

    系列简介

    记得小时候读过一本关于计算机图形学(computer graphics, CG)的入门书,从此就爱上了CG。本系列希望,采用很多人认识的JavaScript语言去分享CG,令更多人有机会接触,并爱上CG。

    本系列的特点之一,是读者能在浏览器里直接执行代码,也可重覆修改代码测试。透过这种互动,也许能更深刻体会内容。读者只要懂得JavaScript(因为JavaScript很简单,学过Java/C/C++/C#之类的语言也应没问题)和一点点线性代数(linear algebra)就可以了。

    笔者在大学期间并没有修读CG课程,虽然看过相关书籍,始终未亲手做过全域光照的渲染器,本文也作为个人的学习分享。此外,笔者也差不多十年没接触JavaScript,希望各位不吝赐教。

    本文简介

    多数程序员听到3D CG,就会联想到Direct3D、OpenGL等API。事实上,这些流行的API主要为实时渲染(real-time rendering)而设,一般采用光栅化(rasterization)方式,渲染大量的三角形(或其他几何图元种类(primitive types))。这种基于光栅化的渲染系统,只支持局部光照(local illumination)。换句话说,渲染几何图形的一个像素时,光照计算只能取得该像素的资讯,而不能访问其他几何图形资讯。理论上,阴影(shadow)、反射(reflection)、折射(refraction)等为全局光照(global illumination)效果,实际上,栅格化渲染系统可以使用预处理(如阴影贴图(shadow mapping)、环境贴图(environment mapping))去模拟这些效果。

    全局光照计算量大,一般也没有特殊硬件加速(通常只使用CPU而非GPU),所以只适合离线渲染(offline rendering),例如3D Studio Max、Maya等工具。其中一个支持全局光照的方法,称为光线追踪(ray tracing)。光线追踪能简单直接地支持阴影、反射、折射,实现起来亦非常容易。本文的例子里,只用了数十行JavaScript代码(除canvas外不需要其他特殊插件和库),就能实现一个支持反射的光线追踪渲染器。光线追踪可以用来学习很多计算机图形学的课题,也许比学习Direct3D/OpenGL更容易。现在,先介绍点理论吧。

    光线追踪

    光栅化渲染,简单地说,就是把大量三角形画到屏幕上。当中会采用深度缓冲(depth buffer, z-buffer),来解决多个三角形重叠时的前后问题。三角形数目影响效能,但三角形在屏幕上的总面积才是主要瓶颈。

    光线追踪,简单地说,就是从摄影机的位置,通过影像平面上的像素位置(比较正确的说法是取样(sampling)位置),发射一束光线到场景,求光线和几何图形间最近的交点,再求该交点的著色。如果该交点的材质是反射性的,可以在该交点向反射方向继续追踪。光线追踪除了容易支持一些全局光照效果外,亦不局限于三角形作为几何图形的单位。任何几何图形,能与一束光线计算交点(intersection point),就能支持。

    上图(來源)显示了光线追踪的基本方式。要计算一点是否在阴影之内,也只须发射一束光线到光源,检测中间有没有障碍物而已。不过光源和阴影留待下回分解。

    初试画板

    光线追踪的输出只是一个影像(image),所谓影像,就是二维颜色数组。

    要在浏览器内,用JavaScript生成一个影像,目前可以使用HTML 5的<canvas>。但现时Internet Explorer(直至版本8)还不支持<canvas>,其他浏览器如Chrome、Firefox、Opera等就可以。

    以下是一个简单的实验,把每个象素填入颜色,左至右越来越红,上至下越来越绿。

    var canvas = document.getElementById("testCanvas"); var ctx = canvas.getContext("2d"); var w = canvas.attributes.width.value; var h = canvas.attributes.height.value; ctx.fillStyle = "rgb(0,0,0)"; ctx.fillRect(0, 0, w, h); var imgdata = ctx.getImageData(0, 0, w, h); var pixels = imgdata.data; var i = 0; for (var y = 0; y < h; y++) for (var x = 0; x < w; x++) { pixels[i++] = x / w * 255; pixels[i++] = y / h * 255; pixels[i++] = 0; pixels[i++] = 255; } ctx.putImageData(imgdata, 0, 0);

    Run

     

    左邊的canvas定義如下:

    <canvas width="256" height="256" id="testCanvas"></canvas>
    

    修改代码试试看

    • 把第三个pixels[i++] = 0 改为255 (即蓝色全开)
    • 把第四个pixels[i++] = 255 改为128 (alpha=128)
    • 可以只修改两个for循环里面的代码,画一个国际象棋棋盘么?

    这实验说明,从canvas取得的影像资料canvas.getImageData(...).data是个一维数组,该数组每四个元素代表一个象素(按红, 绿, 蓝, alpha排列),这些象素在影像中从上至下、左至右排列。

    解决实验平台的技术问题后,可开始从基础类别开始实现。

    基础类

    笔者使用基于物件(object-based)的方式编写JavaScript。

    三维向量

    三维向量(3D vector)可谓CG里最常用型别了。这里三维向量用Vector3类实现,用(x, y, z)表示。 Vector3亦用来表示空间中的点(point),而不另建类。先看代码:

    Vector3 = function(x, y, z) { this.x = x; this.y = y; this.z = z; };
    
    Vector3.prototype = {
        copy : function() { return new Vector3(this.x, this.y, this.z); },
        length : function() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); },
        sqrLength : function() { return this.x * this.x + this.y * this.y + this.z * this.z; },
        normalize : function() { var inv = 1/this.length(); return new Vector3(this.x * inv, this.y * inv, this.z * inv); },
        negate : function() { return new Vector3(-this.x, -this.y, -this.z); },
        add : function(v) { return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z); },
        subtract : function(v) { return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z); },
        multiply : function(f) { return new Vector3(this.x * f, this.y * f, this.z * f); },
        divide : function(f) { var invf = 1/f; return new Vector3(this.x * invf, this.y * invf, this.z * invf); },
        dot : function(v) { return this.x * v.x + this.y * v.y + this.z * v.z; },
        cross : function(v) { return new Vector3(-this.z * v.y + this.y * v.z, this.z * v.x - this.x * v.z, -this.y * v.x + this.x * v.y); }
    };
    
    Vector3.zero = new Vector3(0, 0, 0);
    

    这些类方法(如normalize、negate、add等),如果传回Vector3类对象,都会传回一个新建构的Vector3。这些三维向量的功能很简单,不在此详述。注意multiply和divide是与纯量(scalar)相乘和相除。

    Vector3.zero用作常量,避免每次重新构建。值得一提,这些常量必需在prototype设定之后才能定义。

    光线

    所谓光线(ray),从一点向某方向发射也。数学上可用参数函数(parametric function)表示:

    当中,o即发谢起点(origin),d为方向。在本文的例子里,都假设d为单位向量(unit vector),因此t为距离。实现如下:

    Ray3 = function(origin, direction) { this.origin = origin; this.direction = direction; }
    
    Ray3.prototype = {
        getPoint : function(t) { return this.origin.add(this.direction.multiply(t)); }
    };
    

    球体

    球体(sphere)是其中一个最简单的立体几何图形。这里只考虑球体的表面(surface),中心点为c、半径为r的球体表面可用等式(equation)表示:

    如前文所述,需要计算光线和球体的最近交点。只要把光线x = r(t)代入球体等式,把该等式求解就是交点。为简化方程,设v=o - c,则:

    因为d为单位向量,所以二次方的系数可以消去。 t的二次方程式的解为

    若根号内为负数,即相交不发生。另外,由于这里只需要取最近的交点,因此正负号只需取负号。代码实现如下:

    Sphere = function(center, radius) { this.center = center; this.radius = radius; };
    
    Sphere.prototype = {
        copy : function() { return new Sphere(this.center.copy(), this.radius.copy()); },
    
        initialize : function() {
            this.sqrRadius = this.radius * this.radius;
        },
    
        intersect : function(ray) {
            var v = ray.origin.subtract(this.center);
            var a0 = v.sqrLength() - this.sqrRadius;
            var DdotV = ray.direction.dot(v);
    
            if (DdotV <= 0) {
                var discr = DdotV * DdotV - a0;
                if (discr >= 0) {
                    var result = new IntersectResult();
                    result.geometry = this;
                    result.distance = -DdotV - Math.sqrt(discr);
                    result.position = ray.getPoint(result.distance);
                    result.normal = result.position.subtract(this.center).normalize();
                    return result;
                }
            }
    
            return IntersectResult.noHit;
        }
    };
    

    实现代码时,尽快用最少的运算剔除没相交的情况(Math.sqrt是比较慢的函数)。另外,预计算了球体半径r的平方,此为一个优化。

    这里用到一个IntersectResult类,这个类只用来记录交点的几何物件(geometry)、距离(distance)、位置(position)和法向量(normal)。 IntersectResult.noHit的geometry为null,代表光线没有和任何几何物件相交。

    IntersectResult = function() {
        this.geometry = null;
        this.distance = 0;
        this.position = Vector3.zero;
        this.normal = Vector3.zero;
    };
    
    IntersectResult.noHit = new IntersectResult();
    

    摄影机

    摄影机在光线追踪系统里,负责把影像的取样位置,生成一束光线。

    由于影像的大小是可变的(多少像素宽x多少像素高),为方便计算,这里设定一个统一的取样座标(sx, sy),以左下角为(0,0),右上角为(1 ,1)。

    从数学角度来说,摄影机透过投影(projection),把三维空间投射到二维空间上。常见的投影有正投影(orthographic projection)、透视投影(perspective projection)等等。这里首先实现透视投影。 ]]>

    透视摄影机

    透视摄影机比较像肉眼和真实摄影机的原理,能表现远小近大的观察方式。透视投影从视点(view point/eye position),向某个方向观察场景,观察的角度范围称为视野(field of view, FOV)。除了定义观察的向前(forward)是那个方向,还需要定义在影像平面中,何谓上下和左右。为简单起见,暂时不考虑宽高不同的影像,FOV同时代表水平和垂直方向的视野角度。

    上图显示,从摄影机上方显示的几个参数。 forward和right分别是向前和向右的单位向量。

    因为视点是固定的,光线的起点不变。要生成光线,只须用取样座标(sx, sy)计算其方向d。留意FOV和s的关系为:

    把sx从[0, 1]映射到[-1,1],就可以用right向量和s,来计算r向量,代码如下:

    PerspectiveCamera = function(eye, front, up, fov) { this.eye = eye; this.front = front; this.refUp = up; this.fov = fov; };
    
    PerspectiveCamera.prototype = {
        initialize : function() {
            this.right = this.front.cross(this.refUp);
            this.up = this.right.cross(this.front);
            this.fovScale = Math.tan(this.fov * 0.5 * Math.PI / 180) * 2;
        },
    
        generateRay : function(x, y) {
            var r = this.right.multiply((x - 0.5) * this.fovScale);
            var u = this.up.multiply((y - 0.5) * this.fovScale);
            return new Ray3(this.eye, this.front.add(r).add(u).normalize());
        }
    };
    

    代码中fov为度数,转为弧度才能使用Math.tan()。另外,fovScale预先乘了2,因为sx映射到[-1,1]每次都要乘以2。 sy和sx的做法一样,把两个在影像平面的向量,加上forward向量,就成为光线方向d。因之后的计算需要,最后把d变成单位向量。

    渲染测试

    写了Vector3、Ray3、Sphere、IntersectResult、Camera五个类之后,终于可以开始渲染一点东西出来!

    基本的做法是遍历影像的取样座标(sx, sy),用Camera把(sx, sy)转为Ray3,和场景(例如Sphere)计算最近交点,把该交点的属性转为颜色,写入影像的相对位置里。

    把不同的属性渲染出来,是CG编程里经常用的测试和调试手法。笔者也是用此方法,修正了一些错误。

    渲染深度

    深度(depth)就是从IntersectResult取得最近相交点的距离,因深度的范围是从零至无限,为了把它显示出来,可以把它的一个区间映射到灰阶。这里用[0, maxDepth]映射至[255, 0],即深度0的像素为白色,深度达maxDepth的像素为黑色。

    // renderDepth.htm
    function renderDepth(canvas, scene, camera, maxDepth) {
        // 从canvas取得imgdata和pixels,跟之前的代码一样
        // ...
    
        scene.initialize();
        camera.initialize();
    
        var i = 0;
        for (var y = 0; y < h; y++) {
            var sy = 1 - y / h;
            for (var x = 0; x < w; x++) {
                var sx = x / w;            
                var ray = camera.generateRay(sx, sy);
                var result = scene.intersect(ray);
                if (result.geometry) {
                    var depth = 255 - Math.min((result.distance / maxDepth) * 255, 255);
                    pixels[i    ] = depth;
                    pixels[i + 1] = depth;
                    pixels[i + 2] = depth;
                    pixels[i + 3] = 255;
                }
                i += 4;
            }
        }
    
        ctx.putImageData(imgdata, 0, 0);
    }

    renderDepth( document.getElementById('depthCanvas'), new Sphere(new Vector3(0, 10, -10), 10), new PerspectiveCamera(new Vector3(0, 10, 10), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90), 20);

    Run

     

    这里的观看方向是,正X轴向右,正Y轴向上,正Z轴向后。

    修改代码试试看

    • 改变球体的位置
    • 改变球体的半径
    • 改变fov(PerspectiveCamera最后的参数)
    • 改变maxDepth(renderDepth最后的参数)
    • 改变摄影机的方向,例如向左转一点点(记得要是单位向量啊!可以用new Vector(...).normalize())

    渲染法向量

    相交测试也计算了几何物件在相交位置的法向量,这里也可把它视觉化。法向量是一个单位向量,其每个元素的范围是[-1, 1]。把单位向量映射到颜色的常用方法为,把(x, y, z)映射至(r, g, b),范围从[-1, 1]映射至[0, 255]。

    // renderNormal.htm
    function renderNormal(canvas, scene, camera) {
        // ...
                if (result.geometry) {
                    pixels[i    ] = (result.normal.x + 1) * 128;
                    pixels[i + 1] = (result.normal.y + 1) * 128;
                    pixels[i + 2] = (result.normal.z + 1) * 128;
                    pixels[i + 3] = 255;
                }
        // ...
    }
    

    renderNormal( document.getElementById('normalCanvas'), new Sphere(new Vector3(0, 10, -10), 10), new PerspectiveCamera(new Vector3(0, 10, 10), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90), 20);

    Run

     

    球体上方的法向量是接近(0, 1, 0),所以是浅绿色(0.5, 1, 0.5)。

    修改代码试试看

    • 从球体的正上方往下看

    材质

    渲染深度和法向量只为测试和调试,要显示物件的"真实"颜色,需要定义该交点向某方向(如往视点的方向)发出的光的颜色,称之为几个图形的材质(material )。

    材质的接口为function sample(ray, posiiton, normal) ,传回颜色Color的对象。这是个极简陋的接口,临时做一些效果出来,有机会再详谈。

    颜色

    颜色在CG里最简单是用红、绿、蓝三个通道(color channel)。为实现简单的Phong材质,还加入了对颜色的简单操作。

    Color = function(r, g, b) { this.r = r; this.g = g; this.b = b };
    
    Color.prototype = {
        copy : function() { return new Color(this.r, this.g, this.b); },
        add : function(c) { return new Color(this.r + c.r, this.g + c.g, this.b + c.b); },
        multiply : function(s) { return new Color(this.r * s, this.g * s, this.b * s); },
        modulate : function(c) { return new Color(this.r * c.r, this.g * c.g, this.b * c.b); }
    };
    
    Color.black = new Color(0, 0, 0);
    Color.white = new Color(1, 1, 1);
    Color.red = new Color(1, 0, 0);
    Color.green = new Color(0, 1, 0);
    Color.blue = new Color(0, 0, 1);
    

    这Color类很像Vector3类,值得留意的是,颜色有调制(modulate)操作,其意义为两个颜色中每个颜色通道相乘。

    格子材质

    CG世界里,国际象棋棋盘是最常见的测试用纹理(texture)。这里不考虑纹理贴图(texture mapping)的问题,只凭(x, z)坐标计算某位置发出黑色或白色的光(黑色的光不叫光吧,哈哈)。

    CheckerMaterial = function(scale, reflectiveness) { this.scale = scale; this.reflectiveness = reflectiveness; };
    
    CheckerMaterial.prototype = {
        sample : function(ray, position, normal) {
            return Math.abs((Math.floor(position.x * 0.1) + Math.floor(position.z * this.scale)) % 2) < 1 ? Color.black : Color.white;
        }
    };
    

    代码中scale的意义为1坐标单位有多少个格子,例如scale=0.1即一个格子的大小为10x10。

    Phong材质

    这里实现简单的Phong材质,因为未有光源系统,只用全域变量设置一个临时的光源方向,并只计算漫射(diffuse)和镜射(specular)。

    PhongMaterial = function(diffuse, specular, shininess, reflectiveness) {
        this.diffuse = diffuse;
        this.specular = specular;
        this.shininess = shininess;
        this.reflectiveness = reflectiveness;
    };
    
    // global temp
    var lightDir = new Vector3(1, 1, 1).normalize();
    var lightColor = Color.white;
    
    PhongMaterial.prototype = {
        sample: function(ray, position, normal) {
            var NdotL = normal.dot(lightDir);
            var H = (lightDir.subtract(ray.direction)).normalize();
            var NdotH = normal.dot(H);
            var diffuseTerm = this.diffuse.multiply(Math.max(NdotL, 0));
            var specularTerm = this.specular.multiply(Math.pow(Math.max(NdotH, 0), this.shininess));
            return lightColor.modulate(diffuseTerm.add(specularTerm));
        }
    };
    

    Phong的内容不在此述。

    渲染材质

    修改之前的渲染代码,当碰到相交时,就向几何对象取得material属性,并调用sample方法函数取得颜色。

    // rayTrace.htm
    function rayTrace(canvas, scene, camera) {
        // ...
                if (result.geometry) {
                    var color = result.geometry.material.sample(ray, result.position, result.normal);
                    pixels[i] = color.r * 255;
                    pixels[i + 1] = color.g * 255;
                    pixels[i + 2] = color.b * 255;
                    pixels[i + 3] = 255;
                }
        // ...
    }
    

    var plane = new Plane(new Vector3(0, 1, 0), 0); var sphere1 = new Sphere(new Vector3(-10, 10, -10), 10); var sphere2 = new Sphere(new Vector3(10, 10, -10), 10); plane.material = new CheckerMaterial(0.1); sphere1.material = new PhongMaterial(Color.red, Color.white, 16); sphere2.material = new PhongMaterial(Color.blue, Color.white, 16); rayTrace( document.getElementById('rayTraceCanvas'), new Union([plane, sphere1, sphere2]), new PerspectiveCamera(new Vector3(0, 5, 15), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90));

    Run

     

    修改代码试试看

    • 改变fov,有了格子地板效果应该很明显
    • 改变CheckerMaterial的scale
    • 把原来红色的球改为绿色
    • 把原来红色的球改为黄色
    • 改变shininess(PhongMaterial最后一个参数)

    多个几何物件

    只渲染一个几何物件太乏味,这节再加入一个无限平面,和介绍如何组合多个几何物件。

    平面

    一个(无限)平面(Plane)在数学上可用等式定义:

    n为平面的法向量,d为空间原点至平面的最短距离。光线和平面的相交计算很简单,这里不详述了。

    Plane = function(normal, d) { this.normal = normal; this.d = d; };
    
    Plane.prototype = {
        copy : function() { return new plane(this.normal.copy(), this.d); },
    
        initialize : function() {
            this.position = this.normal.multiply(this.d);
        },
        
        intersect : function(ray) {
            var a = ray.direction.dot(this.normal);
            if (a >= 0)
                return IntersectResult.noHit;
    
            var b = this.normal.dot(ray.origin.subtract(this.position));
            var result = new IntersectResult();
            result.geometry = this;
            result.distance = -b / a;
            result.position = ray.getPoint(result.distance);
            result.normal = this.normal;
            return result;
        }
    };
    

    并集

    把多个几何物件结合起来,可以使用集(set)的概念。这里最容易实现的操作,就是并集(union),即光线要找到一组几个图形的最近交点。无需改其他代码,只加入一个Union类就可以:

    Union = function(geometries) { this.geometries = geometries; };
    
    Union.prototype = {
        initialize: function() {
            for (var i in this.geometries)
                this.geometries[i].initialize();
        },
        
        intersect: function(ray) {
            var minDistance = Infinity;
            var minResult = IntersectResult.noHit;
            for (var i in this.geometries) {
                var result = this.geometries[i].intersect(ray);
                if (result.geometry && result.distance < minDistance) {
                    minDistance = result.distance;
                    minResult = result;
                }
            }
            return minResult;
        }
    };
    

    可以看到,这里利用Javascript的多型(polymorphism)的特性,完全不用修改原来的代码,就可以扩展功能。

    如前所述,这里只考虑几何几何图形的表面。如果考虑几何图形是实心的,就可以用构造实体几何(constructive solid geometry, CSG)方法,提供并集、交集、补集等操作。容后再谈。

    反射

    以上实现的,也只是局部照明。只要再加入一点点代码,就可以实现反射。

    下图说明反射向量的计算方法:

    把d投射到n上(因n是单位向量,只需要点乘即可),就可以计算d在n上的长度,把d减去这长度两倍的法向量,就是反射向量r。数学上可写成:

    一般材质并非完全反射(镜子除外),因此这里为材质加上一个反射度(reflectiveness)的属性。反射的功能很简单,只要在碰到反射度非零的材质,就继续向反射方向追踪,并把结果按反射度来混合。例如一个材质的反射度为25%,则它传回的颜色是75%本身颜色,加上25%反射传回来的颜色。

    另外,不断反射会做成大量的运算,甚至乎永远不能停止(考虑摄影机在两个镜子中间)。因此要限制反射的次数。含反射功能的光线追踪代码如下:

    function rayTraceRecursive(scene, ray, maxReflect) {
        var result = scene.intersect(ray);
        
        if (result.geometry) {
            var reflectiveness = result.geometry.material.reflectiveness;
            var color = result.geometry.material.sample(ray, result.position, result.normal);
            color = color.multiply(1 - reflectiveness);
            
            if (reflectiveness > 0 && maxReflect > 0) {
                var r = result.normal.multiply(-2 * result.normal.dot(ray.direction)).add(ray.direction);
                ray = new Ray3(result.position, r);
                var reflectedColor = rayTraceRecursive(scene, ray, maxReflect - 1);
                color = color.add(reflectedColor.multiply(reflectiveness));
            }
            return color;
        }
        else
            return Color.black;
    }
    
    function rayTraceReflection(canvas, scene, camera, maxReflect) {
        // 从canvas取得imgdata和pixels,跟之前的代码一样
        // ...
    
        scene.initialize();
        camera.initialize();
    
        var i = 0;
        for (var y = 0; y < h; y++) {
            var sy = 1 - y / h;
            for (var x = 0; x < w; x++) {
                var sx = x / w;
                var ray = camera.generateRay(sx, sy);
                var color = rayTraceRecursive(scene, ray, maxReflect);
                pixels[i++] = color.r * 255;
                pixels[i++] = color.g * 255;
                pixels[i++] = color.b * 255;
                pixels[i++] = 255;
            }
        }
    
        ctx.putImageData(imgdata, 0, 0);
    }
    

    var plane = new Plane(new Vector3(0, 1, 0), 0); var sphere1 = new Sphere(new Vector3(-10, 10, -10), 10); var sphere2 = new Sphere(new Vector3(10, 10, -10), 10); plane.material = new CheckerMaterial(0.1, 0.5); sphere1.material = new PhongMaterial(Color.red, Color.white, 16, 0.25); sphere2.material = new PhongMaterial(Color.blue, Color.white, 16, 0.25); rayTraceReflection( document.getElementById('rayTraceReflectionCanvas'), new Union([plane, sphere1, sphere2]), new PerspectiveCamera(new Vector3(0, 5, 15), new Vector3(0, 0, -1), new Vector3(0, 1, 0), 90), 3);

    Run

     

    修改代码试试看

    • 改变一个球的reflectiveness,试试0、1及之间的数值
    • 改变maxReflect(rayTraceReflection最后一个参数)
    • 加入更多的球体(可用for循环啊……不过小心渲染时间太长)

    结语

    能体会到计算机图形学的有趣之处么?百多行简单的JavaScript代码,就绘画出像真的影像,那种满足感实非笔墨所能形容。

    本文实现了一个简单的光线追踪渲染器,支持球体、平面、Phong材质、格子材质、多重反射等功能。读者可以下载这组代码,加入不同的扩展,也可以尝试翻译做熟悉的编程语言。很多光线追踪用到的计算机图形技术,也可以应用到实时图形编程里,例如光源和材质的计算,基本上可以简易翻译做实时图形的著色器(shader)编程。

    游戏里采用光栅化渲染技术已有二十年以上,这几年的硬件发展,使其他渲染方法也能用于实时应用。光线追踪和其他类似的方法,有个当今重要优点,就是能高度平行化。采样之间并没有依赖性,例如256x256=65536个采样,理论上,可使用65536个机器/核心独立执行追踪,那么完成时间只是最慢的一个取样所需的时间。

    笔者希望继续撰写这系列,例如包括以下内容:

    • 其他几何图形(长方体、柱体、三角形、曲面、高度场、等值面、……)
    • 光源(方向光源、点光源、聚光灯、阴影、ambient occlusion)
    • 材质(Phong-Blinn、Oren-Nayar、Torrance-Sparrow、折射、 Fresnel、BRDF、BSDF……)
    • 纹理(纹理座标、采样、Perlin noise)
    • 摄影机模型(正投射、全景、景深)
    • 成像流程(渐进渲染、反锯齿、后期处理)
    • 优化方法(场景剖分、低阶优化)
    • 其他全局光照渲染方法

    祈望得到大家的意见反馈。

    参考

    更新

    • 2010年3月31日,网友HouSisong把本文代码以C++实现,并完全保留了原设计,代码可於他的博文下载。

    评论: 55 查看评论 发表评论

    找优秀程序员,就在博客园


    最新新闻:
    · 雅虎针对iPad推出雅虎娱乐应用(2010-04-02 23:03)
    · 纳斯达克针对iPad推出投资组合管理应用(2010-04-02 23:01)
    · 《时代》杂志iPad应用售价4.99美元(2010-04-02 22:59)
    · 夏普今年将推3D 面板 可裸眼观看3D图片(2010-04-02 22:56)
    · 现有软件编写方式阻碍发挥多核潜力?(2010-04-02 22:14)

    编辑推荐:时代周刊:iPad能否让乔布斯续写传奇

    网站导航:博客园首页  个人主页  新闻  闪存  小组  博问  社区  知识库

  • 气候突变的情景及其对美国国家安全的意义

    最近网上流传着一篇新浪在2004年发表的新闻,新闻上提到了美国政府的智囊团预测了中国在2010年开始南方持续10年大旱。在群里讨论的时候,TK教主给我发了一个PDF文件,这个文件就是美国政府在2003年所请智囊团所作的报告,由中国国家气候中心翻译成中文。报告中预测了此次干旱,还有一些其他非常有趣的预测,比如预测出了冬天会延长(这也解释了为什么现在都已经快4月份了,还在继续寒冷下雪)。

    中国南部和欧洲北部的关键区域在 2010 年 前后将发生持续整整 10 年的特大干旱。与此同时,过去几十年比较 干旱的地区,以及传统上从事旱地农业的区域将会持续几年出现暴雨 和河流泛滥

    中国迫切需要大量的食物养活其庞大的人口。季风降水可靠性的降低将对中国产生重大影响。夏季风可以为中国带来降水,但也会引起负面效应,如洪水可使水土流失更加严重。由于水汽蒸发冷却作用的降低,会引起寒冬延长,夏季高温增加;由于降水减少,业已十分紧张的水资源和能源供应将变得更加严重。由于饥寒交迫的中国觊觎俄罗斯和西部邻国的能源,大范围的饥荒将会引起混乱和国际争端。

    No related posts.

    Related posts brought to you by Yet Another Related Posts Plugin.

  • 什么才是好的教育方式

    注:文章末尾更新了一位朋友对于欧洲教育的阐述,很有意思,也值得思考。

    我通常都不喜欢写这种文章,就是告诉别人,什么才是好的,什么是不好的,你应该去做什么,不应该做什么——除非是那种大是大非问题。因为在我看来,在任何问题上,每个人都有自己的观点,自己的看法,自己的体会,没必要破坏这个多元性。

    但这次,我是自己忍不住想谈谈这方面的看法。有时候学习间隙,我会去mitbbs,天涯这些充斥着大坑的地方看看有什么好玩的东西。我经常听到一些言论,比如,国内的科研在进步,在迎头追上; 国内的基础教育非常牛,在国内读完本科或者高中,有知识基础再出国是非常好的。在这里,我无意贬低国内的任何进步,积极的方面。我只是想谈谈,这两天发生的两件事,促使我去思考一些问题,并记录下我的思考,分享给大家。

    这两件事是这样的。

    和菜头在Blog上写了一篇引起了很大反响的博文:【人格担保推荐】关于公正的公开课。我对此产生了一些兴趣,并将视频下载下来看了几课。这门课老师的上课方式从视频中可以清晰体会到,灵活,生动,深刻,难忘。但说实话,我并没有非常震惊的感觉,原因下面会提到。但总的来说,这绝对是一门优秀到极致的授课视频。这个老师上课,第一,没有ppt。第二,不用记笔记(没有强制要求)。第三,你不会想睡觉(超级重要!)。第四,没有人强制灌输给你,什么是对的,什么是错的,什么是优越的,什么是落后的。想象一下你在中国上的所有哲学人文课是什么样的?这个老师上课的方式很简单,提问,引导讨论,总结一下xx体系的历史演变,推荐书籍,完。但当你上完一节课,你会觉得意犹未尽,回味无穷,你会想继续听下一节课。你会不断思考,从而形成你自己的观点,你也会学着去猜测别人怎么思考,从别人的角度去看同一个问题,并努力找出其中的漏洞。你会先满足于自己对于某个问题的看法,然后惊叹于别人对这个问题的看法,然后从新去思考这个问题,不断循环。最后,最关键的是,again,你不会睡觉。因为你一直在思考,大脑一直在运转。

    这里我一直强调睡觉的问题,因为我爱睡觉,特别是上课的时候。可以这样说,我在国内上课,无论什么课,睡觉时间绝对是听课时间的两倍以上,没有例外。我曾经将此归结为体重原因,但当我某段时间体重减少了几十斤后上课睡觉的习惯依然没有变化,我发现原来我错怪了我的体重。

    对于教育方式差异的震撼,我在上学期就已经体会过一次了,这就是为什么我对“公正的公开课”这门课的教课方式没有很惊讶的原因。上学期,我们系有一门课叫“Thinking concurrently”,instructor叫Aaron,是一个美国人。Aaron师从Stanford大牛,本人极度聪明,用另一个美国学生的话来说,他是一个“活的编译器”。其实他的聪明还体现在另一个方面,就是,教课。

    “Thinking concurrently”主要是讲并行程序的设计,这门课没有教科书,没有板书。上过并行设计方面课程的同学应该知道,这种课程非常难教,按照传统的授课方式,没有生动的ppt讲解,没有老师的详细阐述,几乎是很难理解的。但Aaron每次上课说的话加起来不会超过10分钟。他每次做的事情就是,提出一个概念,学生思考讨论,设计实现伪代码,写出来,他自己以及别的学生提出问题。这个过程完毕之后总结出几个问题,学生针对这个问题对实现代码进行修改。有时候一个问题的讨论总是会费上整节课,有的问题就留为作业。有时候学生提出的实现方案不同,他会引导大家讨论,这些方案的不同方法在哪里,a方案的优势和劣势是什么,b方案哪里好,哪里不好。在这个过程中,他不会告诉学生,标准设计方式是什么,性能最好的方式是什么,也不回告诉学生,以后遇到这个问题,你应该如何去设计。他告诉我们的,是以后遇到这类问题,有多少种思考方法,如何认清它们的优缺点并根据应用做出取舍。

    就这样,一学期,从基础的mutexes, semaphores,到STM概念,到lock-free设计,最后到高阶设计语言机制,erlang,haskells,ML语言等等。整整一个学期,我没有上课睡过觉。当然,我不睡觉不是这篇文章的重点,这并不是什么值得夸耀的事情。重点是,由于我此前没有任何background,加上天资愚笨,不敢说自己学得多好,但其他几个课堂上的学生,绝对都是受益匪浅。


    再说一个例子。如果你有兴趣,打开Google(.com),输入Frank Ryan,第五个结果就是关于Frank Ryan的Wiki。请首先检查一下你的翻墙机制工作正常,然后点进去。

    Frank Ryan的介绍是这样的:他是一个美式足球专业运动员,曾经效力过三个队。他最辉煌的时候是带领克里夫兰布朗人队获得分区赛冠军,三次入选足球全明星。

    但鲜为人知的是,他同时还是一名数学博士,理论数学家,然后,Case Western Reserve大学的教授。

    恩,我们对于Frank Ryan的传奇仅仅介绍到此,因为他的职业传奇并不是这里的特点,虽然这确实很吸引我这个疯狂橄榄球迷。这里,我是想介绍一下Frank Ryan的教学方式。

    Frank Ryan在教课的时候,方式是这样的。他在 Case Western Reserve University开过seminal,主题是Complex Analysis,是和他上的另一门课,advanced topics in complex analysis是配套的。这门课的方式很奇怪,Frank Ryan不讲课,是的,他不讲课。相反,在每一节课的开始,他会拿出一副扑克牌,让学生围着一张圆桌坐下,然后开始自己洗牌,发牌。他规定有两张牌叫烂牌,一张黑桃queen和方块queen。总有两个学生会被发到这两张牌。接下来的流程很简单,开始上课,这两个“幸运”的学生,轮流上来开始讲课。讲什么呢?讲上一节课advanced topics in complex analysis的内容,基本都是数学的推导证明。一个学生讲半节课,然后另一个学生讲后半节课,接着上一个学生推导。

    你说,听起来不是很难阿。但实际上,学生讲的都是教科书上的概念以及证明,不幸的是,教科书很薄,很多证明并不详细,基本都是高阶的框架,比如“容易知道xxx”或者“很明显,xxx不会有xxx性质”这种话。如果学生在讲课推导的时候也用这些话,Frank Ryan就开始说话了,很简单一个单词,“why”?为什么你刚才说的连续性质成立,为什么P(x)不收敛?

    有时候,学生能够回答,于是大家继续。有时候,讲解的学生没法回答,于是其他学生开始思考,提出建议。有时候,整个上半课程都会陷于某一个问题的思考和争论之中,时间到了的话,下一个讲课的学生继续刚才的话题,试图去回答Frank Ryan的why问题。如此循环。所以通常来说,第二个半节课讲解的学生看起来好象有点优势:他可以有半节课时间准备一下他要讲的内容。但其实不是这样,没有人会知道,半节课以后,推导和证明会停留在哪里。

    整个学期,这门课就是这样上的。很明显,这种教课方式并没有传统的灌输方式信息量大,可能这些学生一个学期只能学到10个定理,别的学生可能一学期能学50个定理。但是,Frank Ryan的学生非常喜欢他的教学方式,觉得对数学的概念理解更为清楚,这些学生中,后来也出现过数学和计算机科学领域的超级大牛。

    ×××××××××××××××××××××××××××××分割线××××××××××××××××××××××××××××××××

    为什么?
    为什么在传统的授课方式里面,很难讲清楚的一堆并行计算概念,思想,机制,Aaron能不用板书不用大幅度讲课,就能让学生理解得很好?
    为什么桑德尔的公正课也没有板书,也没有知识的灌输,却能深受学生欢迎?
    为什么Ryan上课几乎就是问问题,”why”,却能让学生喜欢上数学并主动去了解那些枯燥的理论证明,而且还有很好的效果?

    原因是,他们在让学生思考。他们并不是告诉学生,这个题该这样解,那个定理该这样证明。他们希望学生知道,在所有的概念理论背后,真正的机理是什么,你应该怎么去思考。

    思考是一个奇妙的东西,只有思考,才能让一个人开发出所有的潜能,只有思考,才能让一个人完全理解某个东西,只有思考,才能让人越来越聪明。只有大量的思考,才能引起某个可能微不足道的创新。

    接下来说说第二件事。看完这个视频,我出于好奇心,从周围朋友中了解了一些他们上课的情况,有一个很奇怪的现象,就是他们觉得上课上得好的老师,大部分是外国人,他们觉得上课无聊的老师,大部分是中国人或者亚洲人。

    奇怪吗?其实不奇怪。

    中国是怎么教学生的?

    初中,针对中考,题海。这道题该这么解,那个定理很重要,证明也很重要,背下来。
    高中,针对高考,题海。这道题该这么解,那个定理很重要,证明也很重要,背下来。
    大学,没有关键的考试了,有的老师开始学会不用看书讲课了,而是看ppt讲课,一节课滔滔不绝自己一个人说完,也不用管下面真得听懂没有。有的老师保持着写板书的好习惯,可惜板书的内容就是书上内容的照抄,下面学生花花一片都在记笔记,以谁的笔记最快,最全,最标准,最好看为荣。学生只需要知道,考试的内容都在ppt或者笔记上,请在教科书上自行定位复习背诵。

    在我朋友里面,觉得上课无聊的,大部分上课都是这种形式。大部分在美国当教授的中国人,他们的几乎大半部分教育生涯都是在中国,他们已经习惯了这样的形式,想变一个更加灵活的教学方式,还真不容易掌握。他们能做的,也就是总结书上内容,放到ppt里,上课开始照本宣科,然后告诉学生,记笔记!考试内容都在笔记里!他们喜欢考试的形式,因为他们觉得考试才能说明一个学生学好没有学好。在研究生院很多课现在都已经取消final exam的情况下,很多中国老师都对考试表达了一种近乎疯狂的偏爱,mid-term和final-exam接踵而来,分数比例还很大。

    请注意,在这里,我并没有表达说传统的板书教育落伍的观念。事实上,传统的板书教育不仅没有落伍,对于某些基础学科来说,还是最关键的方式,因为对于很多高中生或者低年级本科生来说,基础知识体系还不够完善,还缺乏完全独立讨论一个学科的能力。问题在于,你怎么去执行这么一种形式。再说一个例子,我前段时间一直在看MIT 18.06 Linear Algebra的视频,教课的老师叫Gilbert Strang。这门课,从始到终都是老师板书讲解,偶尔提问让学生思考,但是我也没睡着。我没睡着的原因是,Strang对于线性代数的理解已经到了一个极致,他对线性代数,向量的本质,矩阵的本质,矩阵运算的本质,已经理解清楚到令人发指的程度。听他的课,你永远是处于一个“阿,原来还可以这样去思考”的心态的。所以,虽然他很罗唆,虽然他讲的80%的东西我曾经都学过,我还是听得津津有味。每次听完一节课,再想想当初自己上线性代数的课程,想死的心都有了。事实上,Strang讲课速度很慢,到第10节课左右,才开始涉及到矩阵运算的问题,前面都是基础概念的讲解,从向量,纬度,秩,到空间,恨不得让你把所有基础概念理解到和他一样,才肯继续讲下一个内容。

    也就是说,传统的“灌输”方式并不是恶魔,“灌输”也是分几个层次的,取决于你这个授课老师的能力,以及对这门课本身的理解。很不幸的是,在中国教育中,灌输的形式基本都处于最低级阶段,完全不是一个interactive的过程,老师的目标不是“通过几个问题让你更清楚去理解一个东西本质”,而是“给你讲解尽可能多的问题,从而在考试中拿到高分”。仔细想想,你一个学期背了100个定理又如何?考试完之后,还记得多少?而且很多时候,老师对某些问题的理解都不够深刻,更不要奢望他们能想出什么引导的方式来指导学生讨论了。

    有人可能argue,这难道和学生素质没有关系?其实这个和学生素质半点关系都没有。素质是培养出来的,现在在伯克利,斯坦福,MIT这些名校中,有数量不少的abc,他们的research也好,学业也好,思维方式也好,很多都高于从大陆过去的PHD。他们素质比同在这些名校的PHD高?当然不一定,甚至可能纯论智商的话,abc可能完败。他们根本没有经历过千军万马独木桥,那是如何一个残酷的淘汰阿。而且80%在这些学校的大陆过去的人,智商估计都是本校中顶尖的。但可惜的是,不少人输在了起跑线上。一旦十几年的教育方式强加在你的身上后,你很难去摆脱这种方式带给你的束缚,这是一个思维上全方面的束缚,主动性,创新性,发散性。你能想到的所有的方面。

    回到开始。很多人说,国内的科研在进步,在迎头追上; 国内的基础教育非常牛,在国内读完本科或者高中,有知识基础再出国是非常好的。是的,中国在进步,各方面的,不光是房价。这是不可否认的。但大家应该都很清楚,有些进步是脚踏实地,有些进步是揠苗助长。中国科技研究领域到目前为止,没有几个拿得出手的大师,这并不是偶然。如果不在教育方式上改革,中国的很多进步就好像肉猪:成天吃催化剂,三天就肥肥胖胖,里面的密度其实很小,骨架很脆弱,没有核心竞争力。

    课堂,这是一个最核心最普遍的教育方式。所有的学校都会经历这个阶段,如何让我们的下一代不要输在起跑线上,课堂是一个关键点。如何让学生更积极地融入,如何让我这样的懒人上课不睡觉,如何真正培养学生的思考,这些,都是亟需解决的问题,并值得深思的问题。

    ×××××××××××××××××××××××××××××分割线××××××××××××××××××××××××××××××××

    一位署名Guo的朋友在下面留了这么一段言,我觉得很有思考价值,特地附上。

    lz以及楼上很多人只了解英美与中国的高等教育,却对德国、法国等欧洲大陆国家的高等教育一无所知,固然,德国大学教育在尖端科研比不了美国,但起塑造出来的一批批优秀的工程师和专家,却完完全全是lz那种鄙视的应试教育的产物。

    德国正规的大学,在2年前还没有本科一说,从入学到Diplom,学生至少经历5、6年的折磨,通常是7、8年甚至更久,时常面临着考试不过转专业走人的压力。德国大学,比中国还要应试的多,教授上课通常也是PPT居多,但与中国不同的是配以补充大量的练习课又高年级的学生带领做题,使学生到达对课本内容相当高的熟练成度。

    在一些以考试著称的学校,学生的生活只有—做题、考试;没有什么假期(德国考试安排在假期);顶着巨大的压力(三次不过必须更换专业),往往有的BT的考试课学生要专门做整整一个学期的试卷,是为了确保降降及格而已(通常是10%-40%左右的通过率)。

    可是说德国大学的应试程度,远远超过了LZ眼里所不耻中国的大学;但最主要的区别是:德国大学的考试难度,却在英美国、中国的5-10倍以上,这也是为什么德国能不断产出高质量的工程师的原因。

    另一位署名为guo的朋友留言说

    大概说下在德国读硕士, 以工科为例:
    guo所讲的考试方式其实主要发生在在Vordiplom阶段, 也就是对应于中国本科的基础课阶段. 这个阶段作业是比较多, 像微积分这些数学方面的课, 貌似讲完概念之后,做作业就是最好的学习方式了. 当然人家讲概念时会告诉你他们有啥实际用处,不像我们以前学完傅里叶变换不知道这东西有啥鸟用. 不过本科毕业后去德国的老中, 这个阶段一般都是免掉的.

    4-6学期的Vordiplom完了之后要考个Zwischenpruefung(中期考试),这个考试主要是为了保证学生有过硬的基础知识. 人家是精英教育, 只关心教育质量, 而不考虑通过率. 这个考试的难度对老中来说不是太大.

    过了这个考试就进入Hauptstudium了, 这个阶段相当于本科高年级和研究生的基础课阶段. 这时候老师讲课一般都是天马行空了, 没什么固定教材,他们会根据自己的研究心得和当前前沿的东西组织讲稿, 比如我们上模电时教授有时候会给来点强电的东西, 有时又会说说为了保证航天级的芯片上天后不被宇宙射线搞定要做怎样的防护措施.按照这种方式一堂模电课上完之后, 学生可能不知道某个管子的beta值有多大, 但他肯定知道在设计某个电路时应该考虑哪些东西了. 上课时想记笔记基本是没可能了, 德语单词太长, 一个个写咱跟不上速度, 拿我个人来说只能说上课听个似懂非懂,然后下课去图书馆死磕. 总体看来这个阶段和美国的教育方式貌似区别不大.

    至于考试, 凡是上课涉及的东西可能都会考试. 当然考试并一定都是做几个题了, 有要交家庭作业的(Hausarbeit 其实就是小论文), 还有要做东西出来的. 备考没有考古题可做,没有讲义可复习, 哈 老中最怕这了. 但这些还都不变态, 最变态的是口试(Muedlichepruefung). 基本和硕士答辩的感觉差不多了, 其难度在于你不知道会问什么. 在口试中, 挂的原因有很多, 可能你会因为没搞明白一个貌似不重要的概念而挂掉, 也可能因为教授觉得你的德语让他头疼而让你挂掉… 凡此种种, 不一一细说了. 总之教授的理念是:我们的教育就是让学生一毕业就可以胜任工程师的工作.
    我想楼上guo所说的考试难过应该是这个阶段的考试.

    弄完Hauptstudium就是毕业设计和实习了. 毕业设计都是要做东西出来的, 比如前几届有的都是要流片成功的. 实习一般都是在公司做的, 混的可能性基本=0. 以上都搞完之后就可以拿到所谓的Diplom的学位了, 相当于master. 当然现在学制改革了, 也分学士硕士, 但是这种改革是属于那种换汤不换药的改革, 因为人家的教育理念没改.

    最好说说jy教育, 德国人觉得只有最牛x的人才需要上大学. 一般的人做做钳工之类的就好了, 反正当教授赚钱不一定比一个高级技工多. 何况考到了Meister(可以按照英语里master的意思来理解)的技师的社会地位一点都不比教授差. 如果非要做jy那么就要忍受变态的大学生活.

    至于学制太长通过率低, 我觉得还是因为人家生活太好了, 所以人懒. 学1天玩3天能正常毕业才怪了, 我楼下一大叔读了8年了还没毕业. 人家其实智商不低, 他要按老中的这种读法早就毕业了.

    最后说说中德的差距, 其实只有两点: 1. 中国大学里学得死考的松 2. 中国很多不该上大学的人上了大学

    我的看法是,我讨论的更关键的问题是上课的形式,而非整体教育的形式。事实上,不用说德国法国俄罗斯等以培养基础知识出名的学校,就连日本,虽然也面临学生多学习压力大的情况,但从教育质量上看,他们甩我们几条街。就算是美国,stanford,mit等学校非常注重本科的考试和题目,学生的学习压力比大部分中国高校都大。MIT不是流行一句话么,学习,睡觉,社交,你上MIT就只能任选两项,其学习强度可见一斑。

    我之前对于midterm和final exam的解读,主要针对研究生院。对于本科教育,我也承认传统的板书教育和应试教育非常关键。但我对于德国的应试教育方式是不是真的和中国的应试教育方式差不多,并不是很了解。

    ×××××××××××××××××××××××××××××分割线××××××××××××××××××××××××××××××××

    如果这篇文章能引发争论和思考,那就是发这篇文章的最大的意义。

    也许你还会对这些文章感兴趣

    visit the website for more great content.

  • 这张照片很有逻辑的趣味性


    这张照片很有逻辑的趣味性

    甲乙丙三人,

    丙比甲矮一点,

    丙比乙矮得更多。

    已知甲比乙身高,

    由此推理及判断,

    丙比甲矮很多。

    如果丙和甲身高相差不多,那么丙和乙的身高至少一般高。

    事实并非如此。

    这张照片的趣味性在于,如果不看其它照片,只是根据丙和甲的身高而去推断乙,那就容易误判,主要错误就是数据不全而妄推。

    其次是,丙国的人想用照片说明丙和甲一样高大。

    补:此张百度博客帖发出后,被很多网站转贴,其中就有不少人说照片的的人物高矮,是镜头关系,镜头会起到近大远小的作用。这种评论也很有趣,但是这种评论所作的判断是基于什么的?三张握手照片,人在握手时两个人面对镜头的距离会相差很大吗?握手时,只要两人不是一前一后站立,镜头一般不会有近大远小的视觉作用。




    类别:看图说话 查看评论
  • Distributed Version Control is here to stay, baby

    A while ago Jeff and I had Eric Sink on the Stack Overflow Podcast, and we were yammering on about version control, especially the trendy new distributed version control systems, like Mercurial and Git.

    In that podcast, I said, “To me, the fact that they make branching and merging easier just means that your coworkers are more likely to branch and merge, and you’re more likely to be confused.”


    This is what Taco looks like now
    Well, you know, that podcast is not prepared carefully in advance; it’s just a couple of people shooting the breeze. So what usually happens is that we say things that are, to use the technical term, wrong. Usually they are wrong either in details or in spirit, or in details and in spirit, but this time, I was just plain wrong. Like strawberry pizza. Or jalapeño bagels. WRONG.

    Long before this podcast occurred, my team had switched to Mercurial, and the switch really confused me, so I hired someone to check in code for me (just kidding). I did struggle along for a while by memorizing a few key commands, imagining that they were working just like Subversion, but when something didn’t go the way it would have with Subversion, I got confused, and would pretty much just have to run down the hall to get Benjamin or Jacob to help.

    And then my team said, hey you know what? This Mercurial bug-juice is really amazing, we want to actually make a code review product that works with it, and, and, what’s more, we think that there’s a big market providing commercial support and hosting for it (Mercurial itself is freely available under GPL, but a lot of corporations want some kind of support before they’ll use something).

    And I thought, what do I know? But as you know I don’t really make the decisions around here, because “management is a support function,” so they took all the interns, all six of them, and set off to build a product around Mercurial.

    I decided I better figure out what the heck is going on with this “distributed version control” stuff before somebody asks me a question about the products that my company allegedly sells, and I don’t have an answer, and somebody in the blogo-“sphere” writes another article about me junking the sharp.

    And I studied, and studied, and finally figured something out. Which I want to share with you.

    With distributed version control, the distributed part is actually not the most interesting part.

    The interesting part is that these systems think in terms of changes, not in terms of versions.

    That’s a very zen-like thing to say, I know. Traditional version control thinks: OK, I have version 1. And now I have version 2. And now I have version 3.

    And distributed version control thinks, I had nothing. And then I got these changes. And then I got these other changes.

    It’s a different Program Model, so the user model has to change.

    In Subversion, you might think, “bring my version up to date with the main version” or “go back to the previous version.”

    In Mercurial, you think, “get me Jacob’s change set” or “let’s just forget that change set.”

    If you come at Mercurial with a Subversion mindset, things will almost work, but when they don’t, you’ll be confused, unhappy, and unsuccessful, and you’ll hate Mercurial.

    Whereas if you free your mind and reimagine version control, and grok the zen of the difference between thinking about managing the versions vs. thinking about managing the changes, you’ll become enlightened and happy and realize that this is the way version control was meant to work.

    I know, it’s strange... since 1972 everyone was thinking that we were manipulating versions, but, it turned out, surprisingly, that thinking about the changes themselves as first class solved a very important problem: the problem of merging branched code.

    And here is the most important point, indeed, the most important thing that we’ve learned about developer productivity in a decade. It’s so important that it merits a place as the very last opinion piece that I write, so if you only remember one thing, remember this:

    When you manage changes instead of managing versions, merging works better, and therefore, you can branch any time your organizational goals require it, because merging back will be a piece of cake.

    I can’t tell you how many Subversion users have told me the following story: “We tried to branch our code, and that worked fine. But when it came time to merge back, it was a complete nightmare and we had to practically reapply every change by hand, and we swore never again and we developed a new way of developing software using if statements instead of branches.”

    Sometimes they’re even kind of proud of this new, single-trunk invention of theirs. As if it’s a virtue to work around the fact that your version control tool is not doing what it’s meant to do.

    With distributed version control, merges are easy and work fine. So you can actually have a stable branch and a development branch, or create long-lived branches for your QA team where they test things before deployment, or you can create short-lived branches to try out new ideas and see how they work.

    This is too important to miss out on. This is possibly the biggest advance in software development technology in the ten years I’ve been writing articles here.

    Or, to put it another way, I’d go back to C++ before I gave up on Mercurial.

    If you are using Subversion, stop it. Just stop. Subversion = Leeches. Mercurial and Git = Antibiotics. We have better technology now.

    Because so many people dive into Mercurial without fully understanding the new program model, which can leave them thinking that it’s broken and malicious, I wrote a Mercurial tutorial, HgInit.

    Today, when people ask me about that podcast where I dissed DVCS, I tell them that it was just a very carefully planned fake-out of my long time friend and competitor Eric Sink, who makes a non-distributed version control system. Like that time he started selling bug-tracking software, and, to punish him, we sent him a very expensive Fog Creek backpack with a fake form letter that made it look like we were doing so well that expensive backpacks were the standard Christmas gift we were sending every FogBugz customer.

    I seem to have run out the clock on this site. It has been an extreme honor to have you reading my essays over the last ten years. I couldn’t ask for a greater group of readers. Whether you’re one of the hundreds of people who volunteered their time to translate articles into over 40 languages, or the 22,894 people who has taken the time to send me an email, or the 50,838 people who subscribed to the email newsletter, or the 2,262,348 people per year who visited the website and read some of the 1067 articles I’ve written, I sincerely thank you for your attention.

    Need to hire a really great programmer? Want a job that doesn't drive you crazy? Visit the Joel on Software Job Board: Great software jobs, great people.

Previous page Next page